migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
<?php
/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author 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\User_LDAP;
use OCA\User_LDAP\User\Manager;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\Server;
use Psr\Log\LoggerInterface;
class AccessFactory {
private ILDAPWrapper $ldap;
private Helper $helper;
private IConfig $config;
private IUserManager $ncUserManager;
private LoggerInterface $logger;
public function __construct(
ILDAPWrapper $ldap,
Helper $helper,
IConfig $config,
IUserManager $ncUserManager,
LoggerInterface $logger) {
$this->ldap = $ldap;
$this->helper = $helper;
$this->config = $config;
$this->ncUserManager = $ncUserManager;
$this->logger = $logger;
}
public function get(Connection $connection): Access {
/* Each Access instance gets its own Manager instance, see OCA\User_LDAP\AppInfo\Application::register() */
return new Access(
$connection,
$this->ldap,
Server::get(Manager::class),
$this->helper,
$this->config,
$this->ncUserManager,
$this->logger
);
}
}
@@ -0,0 +1,170 @@
<?php
/**
* @copyright Copyright (c) 2017 Roger Szabo <roger.szabo@web.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
*
* @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\User_LDAP\AppInfo;
use Closure;
use OCA\Files_External\Service\BackendService;
use OCA\User_LDAP\Controller\RenewPasswordController;
use OCA\User_LDAP\Events\GroupBackendRegistered;
use OCA\User_LDAP\Events\UserBackendRegistered;
use OCA\User_LDAP\FilesystemHelper;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\GroupPluginManager;
use OCA\User_LDAP\Handler\ExtStorageConfigHandler;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\ILDAPWrapper;
use OCA\User_LDAP\LDAP;
use OCA\User_LDAP\LoginListener;
use OCA\User_LDAP\Notification\Notifier;
use OCA\User_LDAP\SetupChecks\LdapInvalidUuids;
use OCA\User_LDAP\User\Manager;
use OCA\User_LDAP\User_Proxy;
use OCA\User_LDAP\UserPluginManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\IAppContainer;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\Image;
use OCP\IServerContainer;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Share\IManager as IShareManager;
use OCP\User\Events\PostLoginEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class Application extends App implements IBootstrap {
public function __construct() {
parent::__construct('user_ldap');
$container = $this->getContainer();
/**
* Controller
*/
$container->registerService('RenewPasswordController', function (IAppContainer $appContainer) {
/** @var IServerContainer $server */
$server = $appContainer->get(IServerContainer::class);
return new RenewPasswordController(
$appContainer->get('AppName'),
$server->getRequest(),
$appContainer->get('UserManager'),
$server->getConfig(),
$appContainer->get(IL10N::class),
$appContainer->get('Session'),
$server->getURLGenerator()
);
});
$container->registerService(ILDAPWrapper::class, function (IAppContainer $appContainer) {
/** @var IServerContainer $server */
$server = $appContainer->get(IServerContainer::class);
return new LDAP(
$server->getConfig()->getSystemValueString('ldap_log_file')
);
});
}
public function register(IRegistrationContext $context): void {
$context->registerNotifierService(Notifier::class);
$context->registerService(
Manager::class,
function (ContainerInterface $c) {
return new Manager(
$c->get(IConfig::class),
$c->get(FilesystemHelper::class),
$c->get(LoggerInterface::class),
$c->get(IAvatarManager::class),
$c->get(Image::class),
$c->get(IUserManager::class),
$c->get(INotificationManager::class),
$c->get(IShareManager::class),
);
},
// the instance is specific to a lazy bound Access instance, thus cannot be shared.
false
);
$context->registerEventListener(PostLoginEvent::class, LoginListener::class);
$context->registerSetupCheck(LdapInvalidUuids::class);
}
public function boot(IBootContext $context): void {
$context->injectFn(function (
INotificationManager $notificationManager,
IAppContainer $appContainer,
IEventDispatcher $dispatcher,
IGroupManager $groupManager,
User_Proxy $userBackend,
Group_Proxy $groupBackend,
Helper $helper
) {
$configPrefixes = $helper->getServerConfigurationPrefixes(true);
if (count($configPrefixes) > 0) {
$userPluginManager = $appContainer->get(UserPluginManager::class);
$groupPluginManager = $appContainer->get(GroupPluginManager::class);
\OC_User::useBackend($userBackend);
$groupManager->addBackend($groupBackend);
$userBackendRegisteredEvent = new UserBackendRegistered($userBackend, $userPluginManager);
$dispatcher->dispatch('OCA\\User_LDAP\\User\\User::postLDAPBackendAdded', $userBackendRegisteredEvent);
$dispatcher->dispatchTyped($userBackendRegisteredEvent);
$groupBackendRegisteredEvent = new GroupBackendRegistered($groupBackend, $groupPluginManager);
$dispatcher->dispatchTyped($groupBackendRegisteredEvent);
}
});
$context->injectFn(Closure::fromCallable([$this, 'registerBackendDependents']));
\OCP\Util::connectHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
'\OCA\User_LDAP\Helper',
'loginName2UserName'
);
}
private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher): void {
$dispatcher->addListener(
'OCA\\Files_External::loadAdditionalBackends',
function () use ($appContainer) {
$storagesBackendService = $appContainer->get(BackendService::class);
$storagesBackendService->registerConfigHandler('home', function () use ($appContainer) {
return $appContainer->get(ExtStorageConfigHandler::class);
});
}
);
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
abstract class BackendUtility {
protected $access;
/**
* constructor, make sure the subclasses call this one!
* @param Access $access an instance of Access for LDAP interaction
*/
public function __construct(Access $access) {
$this->access = $access;
}
}
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\Mapping\GroupMapping;
use OCA\User_LDAP\Service\UpdateGroupsService;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Events\GroupCreatedEvent;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CheckGroup extends Command {
public function __construct(
private UpdateGroupsService $service,
protected Group_Proxy $backend,
protected Helper $helper,
protected GroupMapping $mapping,
protected IEventDispatcher $dispatcher,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:check-group')
->setDescription('checks whether a group exists on LDAP.')
->addArgument(
'ocName',
InputArgument::REQUIRED,
'the group name as used in Nextcloud, or the LDAP DN'
)
->addOption(
'force',
null,
InputOption::VALUE_NONE,
'ignores disabled LDAP configuration'
)
->addOption(
'update',
null,
InputOption::VALUE_NONE,
'syncs values from LDAP'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->dispatcher->addListener(GroupCreatedEvent::class, fn ($event) => $this->onGroupCreatedEvent($event, $output));
$this->dispatcher->addListener(UserAddedEvent::class, fn ($event) => $this->onUserAddedEvent($event, $output));
$this->dispatcher->addListener(UserRemovedEvent::class, fn ($event) => $this->onUserRemovedEvent($event, $output));
try {
$this->assertAllowed($input->getOption('force'));
$gid = $input->getArgument('ocName');
$wasMapped = $this->groupWasMapped($gid);
if ($this->backend->getLDAPAccess($gid)->stringResemblesDN($gid)) {
$groupname = $this->backend->dn2GroupName($gid);
if ($groupname !== false) {
$gid = $groupname;
}
}
/* Search to trigger mapping for new groups */
$this->backend->getGroups($gid);
$exists = $this->backend->groupExistsOnLDAP($gid, true);
if ($exists === true) {
$output->writeln('The group is still available on LDAP.');
if ($input->getOption('update')) {
$this->backend->getLDAPAccess($gid)->connection->clearCache();
if ($wasMapped) {
$this->service->handleKnownGroups([$gid]);
} else {
$this->service->handleCreatedGroups([$gid]);
}
}
return 0;
} elseif ($wasMapped) {
$output->writeln('The group does not exist on LDAP anymore.');
if ($input->getOption('update')) {
$this->backend->getLDAPAccess($gid)->connection->clearCache();
$this->service->handleRemovedGroups([$gid]);
}
return 0;
} else {
throw new \Exception('The given group is not a recognized LDAP group.');
}
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage(). '</error>');
return 1;
}
}
public function onGroupCreatedEvent(GroupCreatedEvent $event, OutputInterface $output): void {
$output->writeln('<info>The group '.$event->getGroup()->getGID().' was added to Nextcloud with '.$event->getGroup()->count().' users</info>');
}
public function onUserAddedEvent(UserAddedEvent $event, OutputInterface $output): void {
$user = $event->getUser();
$group = $event->getGroup();
$output->writeln('<info>The user '.$user->getUID().' was added to group '.$group->getGID().'</info>');
}
public function onUserRemovedEvent(UserRemovedEvent $event, OutputInterface $output): void {
$user = $event->getUser();
$group = $event->getGroup();
$output->writeln('<info>The user '.$user->getUID().' was removed from group '.$group->getGID().'</info>');
}
/**
* checks whether a group is actually mapped
* @param string $gid the groupname as passed to the command
*/
protected function groupWasMapped(string $gid): bool {
$dn = $this->mapping->getDNByName($gid);
if ($dn !== false) {
return true;
}
$name = $this->mapping->getNameByDN($gid);
return $name !== false;
}
/**
* checks whether the setup allows reliable checking of LDAP group existence
* @throws \Exception
*/
protected function assertAllowed(bool $force): void {
if ($this->helper->haveDisabledConfigurations() && !$force) {
throw new \Exception('Cannot check group existence, because '
. 'disabled LDAP configurations are present.');
}
// we don't check ldapUserCleanupInterval from config.php because this
// action is triggered manually, while the setting only controls the
// background job.
}
private function updateGroup(string $gid, OutputInterface $output, bool $wasMapped): void {
try {
if ($wasMapped) {
$this->service->handleKnownGroups([$gid]);
} else {
$this->service->handleCreatedGroups([$gid]);
}
} catch (\Exception $e) {
$output->writeln('<error>Error while trying to lookup and update attributes from LDAP</error>');
}
}
}
@@ -0,0 +1,162 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User_Proxy;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CheckUser extends Command {
/** @var User_Proxy */
protected $backend;
/** @var Helper */
protected $helper;
/** @var DeletedUsersIndex */
protected $dui;
/** @var UserMapping */
protected $mapping;
public function __construct(User_Proxy $uBackend, Helper $helper, DeletedUsersIndex $dui, UserMapping $mapping) {
$this->backend = $uBackend;
$this->helper = $helper;
$this->dui = $dui;
$this->mapping = $mapping;
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:check-user')
->setDescription('checks whether a user exists on LDAP.')
->addArgument(
'ocName',
InputArgument::REQUIRED,
'the user name as used in Nextcloud, or the LDAP DN'
)
->addOption(
'force',
null,
InputOption::VALUE_NONE,
'ignores disabled LDAP configuration'
)
->addOption(
'update',
null,
InputOption::VALUE_NONE,
'syncs values from LDAP'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
$this->assertAllowed($input->getOption('force'));
$uid = $input->getArgument('ocName');
if ($this->backend->getLDAPAccess($uid)->stringResemblesDN($uid)) {
$username = $this->backend->dn2UserName($uid);
if ($username !== false) {
$uid = $username;
}
}
$wasMapped = $this->userWasMapped($uid);
$exists = $this->backend->userExistsOnLDAP($uid, true);
if ($exists === true) {
$output->writeln('The user is still available on LDAP.');
if ($input->getOption('update')) {
$this->updateUser($uid, $output);
}
return 0;
} elseif ($wasMapped) {
$this->dui->markUser($uid);
$output->writeln('The user does not exists on LDAP anymore.');
$output->writeln('Clean up the user\'s remnants by: ./occ user:delete "'
. $uid . '"');
return 0;
} else {
throw new \Exception('The given user is not a recognized LDAP user.');
}
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage(). '</error>');
return 1;
}
}
/**
* checks whether a user is actually mapped
* @param string $ocName the username as used in Nextcloud
*/
protected function userWasMapped(string $ocName): bool {
$dn = $this->mapping->getDNByName($ocName);
return $dn !== false;
}
/**
* checks whether the setup allows reliable checking of LDAP user existence
* @throws \Exception
*/
protected function assertAllowed(bool $force): void {
if ($this->helper->haveDisabledConfigurations() && !$force) {
throw new \Exception('Cannot check user existence, because '
. 'disabled LDAP configurations are present.');
}
// we don't check ldapUserCleanupInterval from config.php because this
// action is triggered manually, while the setting only controls the
// background job.
}
private function updateUser(string $uid, OutputInterface $output): void {
try {
$access = $this->backend->getLDAPAccess($uid);
$attrs = $access->userManager->getAttributes();
$user = $access->userManager->get($uid);
$avatarAttributes = $access->getConnection()->resolveRule('avatar');
$result = $access->search('objectclass=*', $user->getDN(), $attrs, 1, 0);
foreach ($result[0] as $attribute => $valueSet) {
$output->writeln(' ' . $attribute . ': ');
foreach ($valueSet as $value) {
if (in_array($attribute, $avatarAttributes)) {
$value = '{ImageData}';
}
$output->writeln(' ' . $value);
}
}
$access->batchApplyUserAttributes($result);
} catch (\Exception $e) {
$output->writeln('<error>Error while trying to lookup and update attributes from LDAP</error>');
}
}
}
@@ -0,0 +1,72 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Martin Konrad <konrad@frib.msu.edu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\Helper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CreateEmptyConfig extends Command {
/** @var \OCA\User_LDAP\Helper */
protected $helper;
/**
* @param Helper $helper
*/
public function __construct(Helper $helper) {
$this->helper = $helper;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:create-empty-config')
->setDescription('creates an empty LDAP configuration')
->addOption(
'only-print-prefix',
'p',
InputOption::VALUE_NONE,
'outputs only the prefix'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$configPrefix = $this->helper->getNextServerConfigurationPrefix();
$configHolder = new Configuration($configPrefix);
$configHolder->ldapConfigurationActive = false;
$configHolder->saveConfiguration();
$prose = '';
if (!$input->getOption('only-print-prefix')) {
$prose = 'Created new configuration with configID ';
}
$output->writeln($prose . "{$configPrefix}");
return 0;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Martin Konrad <info@martin-konrad.net>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Helper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DeleteConfig extends Command {
/** @var \OCA\User_LDAP\Helper */
protected $helper;
/**
* @param Helper $helper
*/
public function __construct(Helper $helper) {
$this->helper = $helper;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:delete-config')
->setDescription('deletes an existing LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$configPrefix = $input->getArgument('configID');
$success = $this->helper->deleteServerConfiguration($configPrefix);
if ($success) {
$output->writeln("Deleted configuration with configID '{$configPrefix}'");
return 0;
} else {
$output->writeln("Cannot delete configuration with configID '{$configPrefix}'");
return 1;
}
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Command;
use OCA\User_LDAP\Group_Proxy;
use OCP\IGroup;
use OCP\IGroupManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class PromoteGroup extends Command {
public function __construct(
private IGroupManager $groupManager,
private Group_Proxy $backend
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:promote-group')
->setDescription('declares the specified group as admin group (only one is possible per LDAP configuration)')
->addArgument(
'group',
InputArgument::REQUIRED,
'the group ID in Nextcloud or a group name'
)
->addOption(
'yes',
'y',
InputOption::VALUE_NONE,
'do not ask for confirmation'
);
}
protected function formatGroupName(IGroup $group): string {
$idLabel = '';
if ($group->getGID() !== $group->getDisplayName()) {
$idLabel = sprintf(' (Group ID: %s)', $group->getGID());
}
return sprintf('%s%s', $group->getDisplayName(), $idLabel);
}
protected function promoteGroup(IGroup $group, InputInterface $input, OutputInterface $output): void {
$access = $this->backend->getLDAPAccess($group->getGID());
$currentlyPromotedGroupId = $access->connection->ldapAdminGroup;
if ($currentlyPromotedGroupId === $group->getGID()) {
$output->writeln('<info>The specified group is already promoted</info>');
return;
}
if ($input->getOption('yes') === false) {
$currentlyPromotedGroup = $this->groupManager->get($currentlyPromotedGroupId);
$demoteLabel = '';
if ($currentlyPromotedGroup instanceof IGroup && $this->backend->groupExists($currentlyPromotedGroup->getGID())) {
$groupNameLabel = $this->formatGroupName($currentlyPromotedGroup);
$demoteLabel = sprintf('and demote %s ', $groupNameLabel);
}
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$q = new Question(sprintf('Promote %s to the admin group %s(y|N)? ', $this->formatGroupName($group), $demoteLabel));
$input->setOption('yes', $helper->ask($input, $output, $q) === 'y');
}
if ($input->getOption('yes') === true) {
$access->connection->setConfiguration(['ldapAdminGroup' => $group->getGID()]);
$access->connection->saveConfiguration();
$output->writeln(sprintf('<info>Group %s was promoted</info>', $group->getDisplayName()));
} else {
$output->writeln('<comment>Group promotion cancelled</comment>');
}
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$groupInput = (string)$input->getArgument('group');
$group = $this->groupManager->get($groupInput);
if ($group instanceof IGroup && $this->backend->groupExists($group->getGID())) {
$this->promoteGroup($group, $input, $output);
return 0;
}
$groupCandidates = $this->backend->getGroups($groupInput, 20);
foreach ($groupCandidates as $gidCandidate) {
$group = $this->groupManager->get($gidCandidate);
if ($group !== null
&& $this->backend->groupExists($group->getGID()) // ensure it is an LDAP group
&& ($group->getGID() === $groupInput
|| $group->getDisplayName() === $groupInput)
) {
$this->promoteGroup($group, $input, $output);
return 0;
}
}
$output->writeln('<error>No matching group found</error>');
return 1;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Command;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\GroupPluginManager;
use OCP\IGroup;
use OCP\IGroupManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class ResetGroup extends Command {
private IGroupManager $groupManager;
private GroupPluginManager $pluginManager;
private Group_Proxy $backend;
public function __construct(
IGroupManager $groupManager,
GroupPluginManager $pluginManager,
Group_Proxy $backend
) {
$this->groupManager = $groupManager;
$this->pluginManager = $pluginManager;
$this->backend = $backend;
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:reset-group')
->setDescription('deletes an LDAP group independent of the group state in the LDAP')
->addArgument(
'gid',
InputArgument::REQUIRED,
'the group name as used in Nextcloud'
)
->addOption(
'yes',
'y',
InputOption::VALUE_NONE,
'do not ask for confirmation'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
$gid = $input->getArgument('gid');
$group = $this->groupManager->get($gid);
if (!$group instanceof IGroup) {
throw new \Exception('Group not found');
}
$backends = $group->getBackendNames();
if (!in_array('LDAP', $backends)) {
throw new \Exception('The given group is not a recognized LDAP group.');
}
if ($input->getOption('yes') === false) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$q = new Question('Delete all local data of this group (y|N)? ');
$input->setOption('yes', $helper->ask($input, $output, $q) === 'y');
}
if ($input->getOption('yes') !== true) {
throw new \Exception('Reset cancelled by operator');
}
// Disable real deletion if a plugin supports it
$pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true);
// Bypass groupExists test to force mapping deletion
$this->backend->getLDAPAccess($gid)->connection->writeToCache('groupExists' . $gid, false);
echo "calling delete $gid\n";
if ($group->delete()) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
return 0;
}
} catch (\Throwable $e) {
if (isset($pluginManagerSuppressed)) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
}
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln('<error>Error while resetting group</error>');
return 2;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Command;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User_Proxy;
use OCA\User_LDAP\UserPluginManager;
use OCP\IUser;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class ResetUser extends Command {
/** @var DeletedUsersIndex */
protected $dui;
/** @var IUserManager */
private $userManager;
/** @var UserPluginManager */
private $pluginManager;
public function __construct(
DeletedUsersIndex $dui,
IUserManager $userManager,
UserPluginManager $pluginManager
) {
$this->dui = $dui;
$this->userManager = $userManager;
$this->pluginManager = $pluginManager;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:reset-user')
->setDescription('deletes an LDAP user independent of the user state')
->addArgument(
'uid',
InputArgument::REQUIRED,
'the user id as used in Nextcloud'
)
->addOption(
'yes',
'y',
InputOption::VALUE_NONE,
'do not ask for confirmation'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
$uid = $input->getArgument('uid');
$user = $this->userManager->get($uid);
if (!$user instanceof IUser) {
throw new \Exception('User not found');
}
$backend = $user->getBackend();
if (!$backend instanceof User_Proxy) {
throw new \Exception('The given user is not a recognized LDAP user.');
}
if ($input->getOption('yes') === false) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$q = new Question('Delete all local data of this user (y|N)? ');
$input->setOption('yes', $helper->ask($input, $output, $q) === 'y');
}
if ($input->getOption('yes') !== true) {
throw new \Exception('Reset cancelled by operator');
}
$this->dui->markUser($uid);
$pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true);
if ($user->delete()) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
return 0;
}
} catch (\Throwable $e) {
if (isset($pluginManagerSuppressed)) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
}
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln('<error>Error while resetting user</error>');
return 2;
}
}
@@ -0,0 +1,140 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\LDAP;
use OCA\User_LDAP\User_Proxy;
use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Search extends Command {
/** @var \OCP\IConfig */
protected $ocConfig;
/** @var User_Proxy */
private $userProxy;
/** @var Group_Proxy */
private $groupProxy;
public function __construct(IConfig $ocConfig, User_Proxy $userProxy, Group_Proxy $groupProxy) {
parent::__construct();
$this->ocConfig = $ocConfig;
$this->userProxy = $userProxy;
$this->groupProxy = $groupProxy;
}
protected function configure() {
$this
->setName('ldap:search')
->setDescription('executes a user or group search')
->addArgument(
'search',
InputArgument::REQUIRED,
'the search string (can be empty)'
)
->addOption(
'group',
null,
InputOption::VALUE_NONE,
'searches groups instead of users'
)
->addOption(
'offset',
null,
InputOption::VALUE_REQUIRED,
'The offset of the result set. Needs to be a multiple of limit. defaults to 0.',
'0'
)
->addOption(
'limit',
null,
InputOption::VALUE_REQUIRED,
'limit the results. 0 means no limit, defaults to 15',
'15'
)
;
}
/**
* Tests whether the offset and limit options are valid
* @param int $offset
* @param int $limit
* @throws \InvalidArgumentException
*/
protected function validateOffsetAndLimit($offset, $limit) {
if ($limit < 0) {
throw new \InvalidArgumentException('limit must be 0 or greater');
}
if ($offset < 0) {
throw new \InvalidArgumentException('offset must be 0 or greater');
}
if ($limit === 0 && $offset !== 0) {
throw new \InvalidArgumentException('offset must be 0 if limit is also set to 0');
}
if ($offset > 0 && ($offset % $limit !== 0)) {
throw new \InvalidArgumentException('offset must be a multiple of limit');
}
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$helper = new Helper($this->ocConfig, \OC::$server->getDatabaseConnection());
$configPrefixes = $helper->getServerConfigurationPrefixes(true);
$ldapWrapper = new LDAP();
$offset = (int)$input->getOption('offset');
$limit = (int)$input->getOption('limit');
$this->validateOffsetAndLimit($offset, $limit);
if ($input->getOption('group')) {
$proxy = $this->groupProxy;
$getMethod = 'getGroups';
$printID = false;
// convert the limit of groups to null. This will show all the groups available instead of
// nothing, and will match the same behaviour the search for users has.
if ($limit === 0) {
$limit = null;
}
} else {
$proxy = $this->userProxy;
$getMethod = 'getDisplayNames';
$printID = true;
}
$result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset);
foreach ($result as $id => $name) {
$line = $name . ($printID ? ' ('.$id.')' : '');
$output->writeln($line);
}
return 0;
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\ConnectionFactory;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\LDAP;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SetConfig extends Command {
protected function configure() {
$this
->setName('ldap:set-config')
->setDescription('modifies an LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
->addArgument(
'configKey',
InputArgument::REQUIRED,
'the configuration key'
)
->addArgument(
'configValue',
InputArgument::REQUIRED,
'the new configuration value'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
$availableConfigs = $helper->getServerConfigurationPrefixes();
$configID = $input->getArgument('configID');
if (!in_array($configID, $availableConfigs)) {
$output->writeln("Invalid configID");
return 1;
}
$this->setValue(
$configID,
$input->getArgument('configKey'),
$input->getArgument('configValue')
);
return 0;
}
/**
* save the configuration value as provided
* @param string $configID
* @param string $configKey
* @param string $configValue
*/
protected function setValue($configID, $key, $value) {
$configHolder = new Configuration($configID);
$configHolder->$key = $value;
$configHolder->saveConfiguration();
$connectionFactory = new ConnectionFactory(new LDAP());
$connectionFactory->get($configID)->clearCache();
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Johannes Leuker <j.leuker@hosting.de>
* @author Laurens Post <Crote@users.noreply.github.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OC\Core\Command\Base;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\Helper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ShowConfig extends Base {
/** @var \OCA\User_LDAP\Helper */
protected $helper;
/**
* @param Helper $helper
*/
public function __construct(Helper $helper) {
$this->helper = $helper;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:show-config')
->setDescription('shows the LDAP configuration')
->addArgument(
'configID',
InputArgument::OPTIONAL,
'will show the configuration of the specified id'
)
->addOption(
'show-password',
null,
InputOption::VALUE_NONE,
'show ldap bind password'
)
->addOption(
'output',
null,
InputOption::VALUE_OPTIONAL,
'Output format (table, plain, json or json_pretty, default is table)',
'table'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$availableConfigs = $this->helper->getServerConfigurationPrefixes();
$configID = $input->getArgument('configID');
if (!is_null($configID)) {
$configIDs[] = $configID;
if (!in_array($configIDs[0], $availableConfigs)) {
$output->writeln("Invalid configID");
return 1;
}
} else {
$configIDs = $availableConfigs;
}
$this->renderConfigs($configIDs, $input, $output);
return 0;
}
/**
* prints the LDAP configuration(s)
* @param string[] configID(s)
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function renderConfigs($configIDs, $input, $output) {
$renderTable = $input->getOption('output') === 'table' or $input->getOption('output') === null;
$showPassword = $input->getOption('show-password');
$configs = [];
foreach ($configIDs as $id) {
$configHolder = new Configuration($id);
$configuration = $configHolder->getConfiguration();
ksort($configuration);
$rows = [];
if ($renderTable) {
foreach ($configuration as $key => $value) {
if (is_array($value)) {
$value = implode(';', $value);
}
if ($key === 'ldapAgentPassword' && !$showPassword) {
$rows[] = [$key, '***'];
} else {
$rows[] = [$key, $value];
}
}
$table = new Table($output);
$table->setHeaders(['Configuration', $id]);
$table->setRows($rows);
$table->render();
} else {
foreach ($configuration as $key => $value) {
if ($key === 'ldapAgentPassword' && !$showPassword) {
$rows[$key] = '***';
} else {
$rows[$key] = $value;
}
}
$configs[$id] = $rows;
}
}
if (!$renderTable) {
$this->writeArrayInOutputFormat($input, $output, $configs);
}
}
}
@@ -0,0 +1,108 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author scolebrook <scolebrook@mac.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCP\IDateTimeFormatter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ShowRemnants extends Command {
/** @var \OCA\User_LDAP\User\DeletedUsersIndex */
protected $dui;
/** @var \OCP\IDateTimeFormatter */
protected $dateFormatter;
/**
* @param DeletedUsersIndex $dui
* @param IDateTimeFormatter $dateFormatter
*/
public function __construct(DeletedUsersIndex $dui, IDateTimeFormatter $dateFormatter) {
$this->dui = $dui;
$this->dateFormatter = $dateFormatter;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:show-remnants')
->setDescription('shows which users are not available on LDAP anymore, but have remnants in Nextcloud.')
->addOption('json', null, InputOption::VALUE_NONE, 'return JSON array instead of pretty table.')
->addOption('short-date', null, InputOption::VALUE_NONE, 'show dates in Y-m-d format');
}
protected function formatDate(int $timestamp, string $default, bool $showShortDate) {
if (!($timestamp > 0)) {
return $default;
}
if ($showShortDate) {
return date('Y-m-d', $timestamp);
}
return $this->dateFormatter->formatDate($timestamp);
}
/**
* executes the command, i.e. creates and outputs a table of LDAP users marked as deleted
*
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
/** @var \Symfony\Component\Console\Helper\Table $table */
$table = new Table($output);
$table->setHeaders([
'Nextcloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login',
'Detected on', 'Dir', 'Sharer'
]);
$rows = [];
$resultSet = $this->dui->getUsers();
foreach ($resultSet as $user) {
$rows[] = [
'ocName' => $user->getOCName(),
'displayName' => $user->getDisplayName(),
'uid' => $user->getUID(),
'dn' => $user->getDN(),
'lastLogin' => $this->formatDate($user->getLastLogin(), '-', (bool)$input->getOption('short-date')),
'detectedOn' => $this->formatDate($user->getDetectedOn(), 'unknown', (bool)$input->getOption('short-date')),
'homePath' => $user->getHomePath(),
'sharer' => $user->getHasActiveShares() ? 'Y' : 'N',
];
}
if ($input->getOption('json')) {
$output->writeln(json_encode($rows));
} else {
$table->setRows($rows);
$table->render();
}
return 0;
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\AccessFactory;
use OCA\User_LDAP\Connection;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\ILDAPWrapper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TestConfig extends Command {
protected const ESTABLISHED = 0;
protected const CONF_INVALID = 1;
protected const BINDFAILURE = 2;
protected const SEARCHFAILURE = 3;
protected AccessFactory $accessFactory;
protected Helper $helper;
protected ILDAPWrapper $ldap;
public function __construct(
AccessFactory $accessFactory,
Helper $helper,
ILDAPWrapper $ldap
) {
$this->accessFactory = $accessFactory;
$this->helper = $helper;
$this->ldap = $ldap;
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:test-config')
->setDescription('tests an LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$availableConfigs = $this->helper->getServerConfigurationPrefixes();
$configID = $input->getArgument('configID');
if (!in_array($configID, $availableConfigs)) {
$output->writeln('Invalid configID');
return 1;
}
$result = $this->testConfig($configID);
switch ($result) {
case static::ESTABLISHED:
$output->writeln('The configuration is valid and the connection could be established!');
return 0;
case static::CONF_INVALID:
$output->writeln('The configuration is invalid. Please have a look at the logs for further details.');
break;
case static::BINDFAILURE:
$output->writeln('The configuration is valid, but the bind failed. Please check the server settings and credentials.');
break;
case static::SEARCHFAILURE:
$output->writeln('The configuration is valid and the bind passed, but a simple search on the base fails. Please check the server base setting.');
break;
default:
$output->writeln('Your LDAP server was kidnapped by aliens.');
break;
}
return 1;
}
/**
* Tests the specified connection
*/
protected function testConfig(string $configID): int {
$connection = new Connection($this->ldap, $configID);
// Ensure validation is run before we attempt the bind
$connection->getConfiguration();
if (!$connection->setConfiguration([
'ldap_configuration_active' => 1,
])) {
return static::CONF_INVALID;
}
if (!$connection->bind()) {
return static::BINDFAILURE;
}
$access = $this->accessFactory->get($connection);
$result = $access->countObjects(1);
if (!is_int($result) || ($result <= 0)) {
return static::SEARCHFAILURE;
}
return static::ESTABLISHED;
}
}
@@ -0,0 +1,373 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Access;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\Mapping\AbstractMapping;
use OCA\User_LDAP\Mapping\GroupMapping;
use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\User_Proxy;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
class UuidUpdateReport {
public const UNCHANGED = 0;
public const UNKNOWN = 1;
public const UNREADABLE = 2;
public const UPDATED = 3;
public const UNWRITABLE = 4;
public const UNMAPPED = 5;
public $id = '';
public $dn = '';
public $isUser = true;
public $state = self::UNCHANGED;
public $oldUuid = '';
public $newUuid = '';
public function __construct(string $id, string $dn, bool $isUser, int $state, string $oldUuid = '', string $newUuid = '') {
$this->id = $id;
$this->dn = $dn;
$this->isUser = $isUser;
$this->state = $state;
$this->oldUuid = $oldUuid;
$this->newUuid = $newUuid;
}
}
class UpdateUUID extends Command {
/** @var UserMapping */
private $userMapping;
/** @var GroupMapping */
private $groupMapping;
/** @var User_Proxy */
private $userProxy;
/** @var Group_Proxy */
private $groupProxy;
/** @var array<UuidUpdateReport[]> */
protected $reports = [];
/** @var LoggerInterface */
private $logger;
/** @var bool */
private $dryRun = false;
public function __construct(UserMapping $userMapping, GroupMapping $groupMapping, User_Proxy $userProxy, Group_Proxy $groupProxy, LoggerInterface $logger) {
$this->userMapping = $userMapping;
$this->groupMapping = $groupMapping;
$this->userProxy = $userProxy;
$this->groupProxy = $groupProxy;
$this->logger = $logger;
$this->reports = [
UuidUpdateReport::UPDATED => [],
UuidUpdateReport::UNKNOWN => [],
UuidUpdateReport::UNREADABLE => [],
UuidUpdateReport::UNWRITABLE => [],
UuidUpdateReport::UNMAPPED => [],
];
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:update-uuid')
->setDescription('Attempts to update UUIDs of user and group entries. By default, the command attempts to update UUIDs that have been invalidated by a migration step.')
->addOption(
'all',
null,
InputOption::VALUE_NONE,
'updates every user and group. All other options are ignored.'
)
->addOption(
'userId',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'a user ID to update'
)
->addOption(
'groupId',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'a group ID to update'
)
->addOption(
'dn',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'a DN to update'
)
->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'UUIDs will not be updated in the database'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->dryRun = $input->getOption('dry-run');
$entriesToUpdate = $this->estimateNumberOfUpdates($input);
$progress = new ProgressBar($output);
$progress->start($entriesToUpdate);
foreach($this->handleUpdates($input) as $_) {
$progress->advance();
}
$progress->finish();
$output->writeln('');
$this->printReport($output);
return count($this->reports[UuidUpdateReport::UNMAPPED]) === 0
&& count($this->reports[UuidUpdateReport::UNREADABLE]) === 0
&& count($this->reports[UuidUpdateReport::UNWRITABLE]) === 0
? 0
: 1;
}
protected function printReport(OutputInterface $output): void {
if ($output->isQuiet()) {
return;
}
if (count($this->reports[UuidUpdateReport::UPDATED]) === 0) {
$output->writeln('<info>No record was updated.</info>');
} else {
$output->writeln(sprintf('<info>%d record(s) were updated.</info>', count($this->reports[UuidUpdateReport::UPDATED])));
if ($output->isVerbose()) {
/** @var UuidUpdateReport $report */
foreach ($this->reports[UuidUpdateReport::UPDATED] as $report) {
$output->writeln(sprintf(' %s had their old UUID %s updated to %s', $report->id, $report->oldUuid, $report->newUuid));
}
$output->writeln('');
}
}
if (count($this->reports[UuidUpdateReport::UNMAPPED]) > 0) {
$output->writeln(sprintf('<error>%d provided IDs were not mapped. These were:</error>', count($this->reports[UuidUpdateReport::UNMAPPED])));
/** @var UuidUpdateReport $report */
foreach ($this->reports[UuidUpdateReport::UNMAPPED] as $report) {
if (!empty($report->id)) {
$output->writeln(sprintf(' %s: %s',
$report->isUser ? 'User' : 'Group', $report->id));
} elseif (!empty($report->dn)) {
$output->writeln(sprintf(' DN: %s', $report->dn));
}
}
$output->writeln('');
}
if (count($this->reports[UuidUpdateReport::UNKNOWN]) > 0) {
$output->writeln(sprintf('<info>%d provided IDs were unknown on LDAP.</info>', count($this->reports[UuidUpdateReport::UNKNOWN])));
if ($output->isVerbose()) {
/** @var UuidUpdateReport $report */
foreach ($this->reports[UuidUpdateReport::UNKNOWN] as $report) {
$output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
}
$output->writeln(PHP_EOL . 'Old users can be removed along with their data per occ user:delete.' . PHP_EOL);
}
}
if (count($this->reports[UuidUpdateReport::UNREADABLE]) > 0) {
$output->writeln(sprintf('<error>For %d records, the UUID could not be read. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNREADABLE])));
if ($output->isVerbose()) {
/** @var UuidUpdateReport $report */
foreach ($this->reports[UuidUpdateReport::UNREADABLE] as $report) {
$output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
}
}
}
if (count($this->reports[UuidUpdateReport::UNWRITABLE]) > 0) {
$output->writeln(sprintf('<error>For %d records, the UUID could not be saved to database. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNWRITABLE])));
if ($output->isVerbose()) {
/** @var UuidUpdateReport $report */
foreach ($this->reports[UuidUpdateReport::UNWRITABLE] as $report) {
$output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
}
}
}
}
protected function handleUpdates(InputInterface $input): \Generator {
if ($input->getOption('all')) {
foreach($this->handleMappingBasedUpdates(false) as $_) {
yield;
}
} elseif ($input->getOption('userId')
|| $input->getOption('groupId')
|| $input->getOption('dn')
) {
foreach($this->handleUpdatesByUserId($input->getOption('userId')) as $_) {
yield;
}
foreach($this->handleUpdatesByGroupId($input->getOption('groupId')) as $_) {
yield;
}
foreach($this->handleUpdatesByDN($input->getOption('dn')) as $_) {
yield;
}
} else {
foreach($this->handleMappingBasedUpdates(true) as $_) {
yield;
}
}
}
protected function handleUpdatesByUserId(array $userIds): \Generator {
foreach($this->handleUpdatesByEntryId($userIds, $this->userMapping) as $_) {
yield;
}
}
protected function handleUpdatesByGroupId(array $groupIds): \Generator {
foreach($this->handleUpdatesByEntryId($groupIds, $this->groupMapping) as $_) {
yield;
}
}
protected function handleUpdatesByDN(array $dns): \Generator {
$userList = $groupList = [];
while ($dn = array_pop($dns)) {
$uuid = $this->userMapping->getUUIDByDN($dn);
if ($uuid) {
$id = $this->userMapping->getNameByDN($dn);
$userList[] = ['name' => $id, 'uuid' => $uuid];
continue;
}
$uuid = $this->groupMapping->getUUIDByDN($dn);
if ($uuid) {
$id = $this->groupMapping->getNameByDN($dn);
$groupList[] = ['name' => $id, 'uuid' => $uuid];
continue;
}
$this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport('', $dn, true, UuidUpdateReport::UNMAPPED);
yield;
}
foreach($this->handleUpdatesByList($this->userMapping, $userList) as $_) {
yield;
}
foreach($this->handleUpdatesByList($this->groupMapping, $groupList) as $_) {
yield;
}
}
protected function handleUpdatesByEntryId(array $ids, AbstractMapping $mapping): \Generator {
$isUser = $mapping instanceof UserMapping;
$list = [];
while ($id = array_pop($ids)) {
if(!$dn = $mapping->getDNByName($id)) {
$this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport($id, '', $isUser, UuidUpdateReport::UNMAPPED);
yield;
continue;
}
// Since we know it was mapped the UUID is populated
$uuid = $mapping->getUUIDByDN($dn);
$list[] = ['name' => $id, 'uuid' => $uuid];
}
foreach($this->handleUpdatesByList($mapping, $list) as $_) {
yield;
}
}
protected function handleMappingBasedUpdates(bool $invalidatedOnly): \Generator {
$limit = 1000;
/** @var AbstractMapping $mapping*/
foreach([$this->userMapping, $this->groupMapping] as $mapping) {
$offset = 0;
do {
$list = $mapping->getList($offset, $limit, $invalidatedOnly);
$offset += $limit;
foreach($this->handleUpdatesByList($mapping, $list) as $tick) {
yield; // null, for it only advances progress counter
}
} while (count($list) === $limit);
}
}
protected function handleUpdatesByList(AbstractMapping $mapping, array $list): \Generator {
if ($mapping instanceof UserMapping) {
$isUser = true;
$backendProxy = $this->userProxy;
} else {
$isUser = false;
$backendProxy = $this->groupProxy;
}
foreach ($list as $row) {
$access = $backendProxy->getLDAPAccess($row['name']);
if ($access instanceof Access
&& $dn = $mapping->getDNByName($row['name'])) {
if ($uuid = $access->getUUID($dn, $isUser)) {
if ($uuid !== $row['uuid']) {
if ($this->dryRun || $mapping->setUUIDbyDN($uuid, $dn)) {
$this->reports[UuidUpdateReport::UPDATED][]
= new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UPDATED, $row['uuid'], $uuid);
} else {
$this->reports[UuidUpdateReport::UNWRITABLE][]
= new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNWRITABLE, $row['uuid'], $uuid);
}
$this->logger->info('UUID of {id} was updated from {from} to {to}',
[
'appid' => 'user_ldap',
'id' => $row['name'],
'from' => $row['uuid'],
'to' => $uuid,
]
);
}
} else {
$this->reports[UuidUpdateReport::UNREADABLE][] = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNREADABLE);
}
} else {
$this->reports[UuidUpdateReport::UNKNOWN][] = new UuidUpdateReport($row['name'], '', $isUser, UuidUpdateReport::UNKNOWN);
}
yield; // null, for it only advances progress counter
}
}
protected function estimateNumberOfUpdates(InputInterface $input): int {
if ($input->getOption('all')) {
return $this->userMapping->count() + $this->groupMapping->count();
} elseif ($input->getOption('userId')
|| $input->getOption('groupId')
|| $input->getOption('dn')
) {
return count($input->getOption('userId'))
+ count($input->getOption('groupId'))
+ count($input->getOption('dn'));
} else {
return $this->userMapping->countInvalidated() + $this->groupMapping->countInvalidated();
}
}
}
@@ -0,0 +1,612 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Alexander Bergolth <leo@strike.wu.ac.at>
* @author Alex Weirig <alex.weirig@technolink.lu>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lennart Rosam <hello@takuto.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Marc Hefter <marchefter@march42.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Xuanwo <xuanwo@yunify.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
/**
* @property int ldapPagingSize holds an integer
* @property string ldapUserAvatarRule
*/
class Configuration {
public const AVATAR_PREFIX_DEFAULT = 'default';
public const AVATAR_PREFIX_NONE = 'none';
public const AVATAR_PREFIX_DATA_ATTRIBUTE = 'data:';
public const LDAP_SERVER_FEATURE_UNKNOWN = 'unknown';
public const LDAP_SERVER_FEATURE_AVAILABLE = 'available';
public const LDAP_SERVER_FEATURE_UNAVAILABLE = 'unavailable';
/**
* @var string
*/
protected $configPrefix;
/**
* @var bool
*/
protected $configRead = false;
/**
* @var string[]
*/
protected array $unsavedChanges = [];
/**
* @var array<string, mixed> settings
*/
protected $config = [
'ldapHost' => null,
'ldapPort' => null,
'ldapBackupHost' => null,
'ldapBackupPort' => null,
'ldapBackgroundHost' => null,
'ldapBackgroundPort' => null,
'ldapBase' => null,
'ldapBaseUsers' => null,
'ldapBaseGroups' => null,
'ldapAgentName' => null,
'ldapAgentPassword' => null,
'ldapTLS' => null,
'turnOffCertCheck' => null,
'ldapIgnoreNamingRules' => null,
'ldapUserDisplayName' => null,
'ldapUserDisplayName2' => null,
'ldapUserAvatarRule' => null,
'ldapGidNumber' => null,
'ldapUserFilterObjectclass' => null,
'ldapUserFilterGroups' => null,
'ldapUserFilter' => null,
'ldapUserFilterMode' => null,
'ldapGroupFilter' => null,
'ldapGroupFilterMode' => null,
'ldapGroupFilterObjectclass' => null,
'ldapGroupFilterGroups' => null,
'ldapGroupDisplayName' => null,
'ldapGroupMemberAssocAttr' => null,
'ldapLoginFilter' => null,
'ldapLoginFilterMode' => null,
'ldapLoginFilterEmail' => null,
'ldapLoginFilterUsername' => null,
'ldapLoginFilterAttributes' => null,
'ldapQuotaAttribute' => null,
'ldapQuotaDefault' => null,
'ldapEmailAttribute' => null,
'ldapCacheTTL' => null,
'ldapUuidUserAttribute' => 'auto',
'ldapUuidGroupAttribute' => 'auto',
'ldapOverrideMainServer' => false,
'ldapConfigurationActive' => false,
'ldapAttributesForUserSearch' => null,
'ldapAttributesForGroupSearch' => null,
'ldapExperiencedAdmin' => false,
'homeFolderNamingRule' => null,
'hasMemberOfFilterSupport' => false,
'useMemberOfToDetectMembership' => true,
'ldapExpertUsernameAttr' => null,
'ldapExpertUUIDUserAttr' => null,
'ldapExpertUUIDGroupAttr' => null,
'markRemnantsAsDisabled' => false,
'lastJpegPhotoLookup' => null,
'ldapNestedGroups' => false,
'ldapPagingSize' => null,
'turnOnPasswordChange' => false,
'ldapDynamicGroupMemberURL' => null,
'ldapDefaultPPolicyDN' => null,
'ldapExtStorageHomeAttribute' => null,
'ldapMatchingRuleInChainState' => self::LDAP_SERVER_FEATURE_UNKNOWN,
'ldapConnectionTimeout' => 15,
'ldapAttributePhone' => null,
'ldapAttributeWebsite' => null,
'ldapAttributeAddress' => null,
'ldapAttributeTwitter' => null,
'ldapAttributeFediverse' => null,
'ldapAttributeOrganisation' => null,
'ldapAttributeRole' => null,
'ldapAttributeHeadline' => null,
'ldapAttributeBiography' => null,
'ldapAdminGroup' => '',
];
public function __construct(string $configPrefix, bool $autoRead = true) {
$this->configPrefix = $configPrefix;
if ($autoRead) {
$this->readConfiguration();
}
}
/**
* @param string $name
* @return mixed|null
*/
public function __get($name) {
if (isset($this->config[$name])) {
return $this->config[$name];
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value) {
$this->setConfiguration([$name => $value]);
}
public function getConfiguration(): array {
return $this->config;
}
/**
* set LDAP configuration with values delivered by an array, not read
* from configuration. It does not save the configuration! To do so, you
* must call saveConfiguration afterwards.
* @param array $config array that holds the config parameters in an associated
* array
* @param array &$applied optional; array where the set fields will be given to
*/
public function setConfiguration(array $config, array &$applied = null): void {
$cta = $this->getConfigTranslationArray();
foreach ($config as $inputKey => $val) {
if (str_contains($inputKey, '_') && array_key_exists($inputKey, $cta)) {
$key = $cta[$inputKey];
} elseif (array_key_exists($inputKey, $this->config)) {
$key = $inputKey;
} else {
continue;
}
$setMethod = 'setValue';
switch ($key) {
case 'ldapAgentPassword':
$setMethod = 'setRawValue';
break;
case 'homeFolderNamingRule':
$trimmedVal = trim($val);
if ($trimmedVal !== '' && !str_contains($val, 'attr:')) {
$val = 'attr:'.$trimmedVal;
}
break;
case 'ldapBase':
case 'ldapBaseUsers':
case 'ldapBaseGroups':
case 'ldapAttributesForUserSearch':
case 'ldapAttributesForGroupSearch':
case 'ldapUserFilterObjectclass':
case 'ldapUserFilterGroups':
case 'ldapGroupFilterObjectclass':
case 'ldapGroupFilterGroups':
case 'ldapLoginFilterAttributes':
$setMethod = 'setMultiLine';
break;
}
$this->$setMethod($key, $val);
if (is_array($applied)) {
$applied[] = $inputKey;
// storing key as index avoids duplication, and as value for simplicity
}
$this->unsavedChanges[$key] = $key;
}
}
public function readConfiguration(): void {
if (!$this->configRead) {
$cta = array_flip($this->getConfigTranslationArray());
foreach ($this->config as $key => $val) {
if (!isset($cta[$key])) {
//some are determined
continue;
}
$dbKey = $cta[$key];
switch ($key) {
case 'ldapBase':
case 'ldapBaseUsers':
case 'ldapBaseGroups':
case 'ldapAttributesForUserSearch':
case 'ldapAttributesForGroupSearch':
case 'ldapUserFilterObjectclass':
case 'ldapUserFilterGroups':
case 'ldapGroupFilterObjectclass':
case 'ldapGroupFilterGroups':
case 'ldapLoginFilterAttributes':
$readMethod = 'getMultiLine';
break;
case 'ldapIgnoreNamingRules':
$readMethod = 'getSystemValue';
$dbKey = $key;
break;
case 'ldapAgentPassword':
$readMethod = 'getPwd';
break;
case 'ldapUserDisplayName2':
case 'ldapGroupDisplayName':
$readMethod = 'getLcValue';
break;
case 'ldapUserDisplayName':
default:
// user display name does not lower case because
// we rely on an upper case N as indicator whether to
// auto-detect it or not. FIXME
$readMethod = 'getValue';
break;
}
$this->config[$key] = $this->$readMethod($dbKey);
}
$this->configRead = true;
}
}
/**
* saves the current config changes in the database
*/
public function saveConfiguration(): void {
$cta = array_flip($this->getConfigTranslationArray());
$changed = false;
foreach ($this->unsavedChanges as $key) {
$value = $this->config[$key];
switch ($key) {
case 'ldapAgentPassword':
$value = base64_encode($value);
break;
case 'ldapBase':
case 'ldapBaseUsers':
case 'ldapBaseGroups':
case 'ldapAttributesForUserSearch':
case 'ldapAttributesForGroupSearch':
case 'ldapUserFilterObjectclass':
case 'ldapUserFilterGroups':
case 'ldapGroupFilterObjectclass':
case 'ldapGroupFilterGroups':
case 'ldapLoginFilterAttributes':
if (is_array($value)) {
$value = implode("\n", $value);
}
break;
//following options are not stored but detected, skip them
case 'ldapIgnoreNamingRules':
case 'ldapUuidUserAttribute':
case 'ldapUuidGroupAttribute':
continue 2;
}
if (is_null($value)) {
$value = '';
}
$changed = true;
$this->saveValue($cta[$key], $value);
}
if ($changed) {
$this->saveValue('_lastChange', (string)time());
}
$this->unsavedChanges = [];
}
/**
* @param string $varName
* @return array|string
*/
protected function getMultiLine($varName) {
$value = $this->getValue($varName);
if (empty($value)) {
$value = '';
} else {
$value = preg_split('/\r\n|\r|\n/', $value);
}
return $value;
}
/**
* Sets multi-line values as arrays
*
* @param string $varName name of config-key
* @param array|string $value to set
*/
protected function setMultiLine(string $varName, $value): void {
if (empty($value)) {
$value = '';
} elseif (!is_array($value)) {
$value = preg_split('/\r\n|\r|\n|;/', $value);
if ($value === false) {
$value = '';
}
}
if (!is_array($value)) {
$finalValue = trim($value);
} else {
$finalValue = [];
foreach ($value as $key => $val) {
if (is_string($val)) {
$val = trim($val);
if ($val !== '') {
//accidental line breaks are not wanted and can cause
// odd behaviour. Thus, away with them.
$finalValue[] = $val;
}
} else {
$finalValue[] = $val;
}
}
}
$this->setRawValue($varName, $finalValue);
}
protected function getPwd(string $varName): string {
return base64_decode($this->getValue($varName));
}
protected function getLcValue(string $varName): string {
return mb_strtolower($this->getValue($varName), 'UTF-8');
}
protected function getSystemValue(string $varName): string {
//FIXME: if another system value is added, softcode the default value
return \OC::$server->getConfig()->getSystemValue($varName, false);
}
protected function getValue(string $varName): string {
static $defaults;
if (is_null($defaults)) {
$defaults = $this->getDefaults();
}
return \OC::$server->getConfig()->getAppValue('user_ldap',
$this->configPrefix.$varName,
$defaults[$varName]);
}
/**
* Sets a scalar value.
*
* @param string $varName name of config key
* @param mixed $value to set
*/
protected function setValue(string $varName, $value): void {
if (is_string($value)) {
$value = trim($value);
}
$this->config[$varName] = $value;
}
/**
* Sets a scalar value without trimming.
*
* @param string $varName name of config key
* @param mixed $value to set
*/
protected function setRawValue(string $varName, $value): void {
$this->config[$varName] = $value;
}
protected function saveValue(string $varName, string $value): bool {
\OC::$server->getConfig()->setAppValue(
'user_ldap',
$this->configPrefix.$varName,
$value
);
return true;
}
/**
* @return array an associative array with the default values. Keys are correspond
* to config-value entries in the database table
*/
public function getDefaults(): array {
return [
'ldap_host' => '',
'ldap_port' => '',
'ldap_backup_host' => '',
'ldap_backup_port' => '',
'ldap_background_host' => '',
'ldap_background_port' => '',
'ldap_override_main_server' => '',
'ldap_dn' => '',
'ldap_agent_password' => '',
'ldap_base' => '',
'ldap_base_users' => '',
'ldap_base_groups' => '',
'ldap_userlist_filter' => '',
'ldap_user_filter_mode' => 0,
'ldap_userfilter_objectclass' => '',
'ldap_userfilter_groups' => '',
'ldap_login_filter' => '',
'ldap_login_filter_mode' => 0,
'ldap_loginfilter_email' => 0,
'ldap_loginfilter_username' => 1,
'ldap_loginfilter_attributes' => '',
'ldap_group_filter' => '',
'ldap_group_filter_mode' => 0,
'ldap_groupfilter_objectclass' => '',
'ldap_groupfilter_groups' => '',
'ldap_gid_number' => 'gidNumber',
'ldap_display_name' => 'displayName',
'ldap_user_display_name_2' => '',
'ldap_group_display_name' => 'cn',
'ldap_tls' => 0,
'ldap_quota_def' => '',
'ldap_quota_attr' => '',
'ldap_email_attr' => '',
'ldap_group_member_assoc_attribute' => '',
'ldap_cache_ttl' => 600,
'ldap_uuid_user_attribute' => 'auto',
'ldap_uuid_group_attribute' => 'auto',
'home_folder_naming_rule' => '',
'ldap_turn_off_cert_check' => 0,
'ldap_configuration_active' => 0,
'ldap_attributes_for_user_search' => '',
'ldap_attributes_for_group_search' => '',
'ldap_expert_username_attr' => '',
'ldap_expert_uuid_user_attr' => '',
'ldap_expert_uuid_group_attr' => '',
'has_memberof_filter_support' => 0,
'use_memberof_to_detect_membership' => 1,
'ldap_mark_remnants_as_disabled' => 0,
'last_jpegPhoto_lookup' => 0,
'ldap_nested_groups' => 0,
'ldap_paging_size' => 500,
'ldap_turn_on_pwd_change' => 0,
'ldap_experienced_admin' => 0,
'ldap_dynamic_group_member_url' => '',
'ldap_default_ppolicy_dn' => '',
'ldap_user_avatar_rule' => 'default',
'ldap_ext_storage_home_attribute' => '',
'ldap_matching_rule_in_chain_state' => self::LDAP_SERVER_FEATURE_UNKNOWN,
'ldap_connection_timeout' => 15,
'ldap_attr_phone' => '',
'ldap_attr_website' => '',
'ldap_attr_address' => '',
'ldap_attr_twitter' => '',
'ldap_attr_fediverse' => '',
'ldap_attr_organisation' => '',
'ldap_attr_role' => '',
'ldap_attr_headline' => '',
'ldap_attr_biography' => '',
'ldap_admin_group' => '',
];
}
/**
* @return array that maps internal variable names to database fields
*/
public function getConfigTranslationArray(): array {
//TODO: merge them into one representation
static $array = [
'ldap_host' => 'ldapHost',
'ldap_port' => 'ldapPort',
'ldap_backup_host' => 'ldapBackupHost',
'ldap_backup_port' => 'ldapBackupPort',
'ldap_background_host' => 'ldapBackgroundHost',
'ldap_background_port' => 'ldapBackgroundPort',
'ldap_override_main_server' => 'ldapOverrideMainServer',
'ldap_dn' => 'ldapAgentName',
'ldap_agent_password' => 'ldapAgentPassword',
'ldap_base' => 'ldapBase',
'ldap_base_users' => 'ldapBaseUsers',
'ldap_base_groups' => 'ldapBaseGroups',
'ldap_userfilter_objectclass' => 'ldapUserFilterObjectclass',
'ldap_userfilter_groups' => 'ldapUserFilterGroups',
'ldap_userlist_filter' => 'ldapUserFilter',
'ldap_user_filter_mode' => 'ldapUserFilterMode',
'ldap_user_avatar_rule' => 'ldapUserAvatarRule',
'ldap_login_filter' => 'ldapLoginFilter',
'ldap_login_filter_mode' => 'ldapLoginFilterMode',
'ldap_loginfilter_email' => 'ldapLoginFilterEmail',
'ldap_loginfilter_username' => 'ldapLoginFilterUsername',
'ldap_loginfilter_attributes' => 'ldapLoginFilterAttributes',
'ldap_group_filter' => 'ldapGroupFilter',
'ldap_group_filter_mode' => 'ldapGroupFilterMode',
'ldap_groupfilter_objectclass' => 'ldapGroupFilterObjectclass',
'ldap_groupfilter_groups' => 'ldapGroupFilterGroups',
'ldap_gid_number' => 'ldapGidNumber',
'ldap_display_name' => 'ldapUserDisplayName',
'ldap_user_display_name_2' => 'ldapUserDisplayName2',
'ldap_group_display_name' => 'ldapGroupDisplayName',
'ldap_tls' => 'ldapTLS',
'ldap_quota_def' => 'ldapQuotaDefault',
'ldap_quota_attr' => 'ldapQuotaAttribute',
'ldap_email_attr' => 'ldapEmailAttribute',
'ldap_group_member_assoc_attribute' => 'ldapGroupMemberAssocAttr',
'ldap_cache_ttl' => 'ldapCacheTTL',
'home_folder_naming_rule' => 'homeFolderNamingRule',
'ldap_turn_off_cert_check' => 'turnOffCertCheck',
'ldap_configuration_active' => 'ldapConfigurationActive',
'ldap_attributes_for_user_search' => 'ldapAttributesForUserSearch',
'ldap_attributes_for_group_search' => 'ldapAttributesForGroupSearch',
'ldap_expert_username_attr' => 'ldapExpertUsernameAttr',
'ldap_expert_uuid_user_attr' => 'ldapExpertUUIDUserAttr',
'ldap_expert_uuid_group_attr' => 'ldapExpertUUIDGroupAttr',
'has_memberof_filter_support' => 'hasMemberOfFilterSupport',
'use_memberof_to_detect_membership' => 'useMemberOfToDetectMembership',
'ldap_mark_remnants_as_disabled' => 'markRemnantsAsDisabled',
'last_jpegPhoto_lookup' => 'lastJpegPhotoLookup',
'ldap_nested_groups' => 'ldapNestedGroups',
'ldap_paging_size' => 'ldapPagingSize',
'ldap_turn_on_pwd_change' => 'turnOnPasswordChange',
'ldap_experienced_admin' => 'ldapExperiencedAdmin',
'ldap_dynamic_group_member_url' => 'ldapDynamicGroupMemberURL',
'ldap_default_ppolicy_dn' => 'ldapDefaultPPolicyDN',
'ldap_ext_storage_home_attribute' => 'ldapExtStorageHomeAttribute',
'ldap_matching_rule_in_chain_state' => 'ldapMatchingRuleInChainState',
'ldapIgnoreNamingRules' => 'ldapIgnoreNamingRules', // sysconfig
'ldap_connection_timeout' => 'ldapConnectionTimeout',
'ldap_attr_phone' => 'ldapAttributePhone',
'ldap_attr_website' => 'ldapAttributeWebsite',
'ldap_attr_address' => 'ldapAttributeAddress',
'ldap_attr_twitter' => 'ldapAttributeTwitter',
'ldap_attr_fediverse' => 'ldapAttributeFediverse',
'ldap_attr_organisation' => 'ldapAttributeOrganisation',
'ldap_attr_role' => 'ldapAttributeRole',
'ldap_attr_headline' => 'ldapAttributeHeadline',
'ldap_attr_biography' => 'ldapAttributeBiography',
'ldap_admin_group' => 'ldapAdminGroup',
];
return $array;
}
/**
* @throws \RuntimeException
*/
public function resolveRule(string $rule): array {
if ($rule === 'avatar') {
return $this->getAvatarAttributes();
}
throw new \RuntimeException('Invalid rule');
}
public function getAvatarAttributes(): array {
$value = $this->ldapUserAvatarRule ?: self::AVATAR_PREFIX_DEFAULT;
$defaultAttributes = ['jpegphoto', 'thumbnailphoto'];
if ($value === self::AVATAR_PREFIX_NONE) {
return [];
}
if (str_starts_with($value, self::AVATAR_PREFIX_DATA_ATTRIBUTE)) {
$attribute = trim(substr($value, strlen(self::AVATAR_PREFIX_DATA_ATTRIBUTE)));
if ($attribute === '') {
return $defaultAttributes;
}
return [strtolower($attribute)];
}
if ($value !== self::AVATAR_PREFIX_DEFAULT) {
\OC::$server->getLogger()->warning('Invalid config value to ldapUserAvatarRule; falling back to default.');
}
return $defaultAttributes;
}
/**
* Returns TRUE if the ldapHost variable starts with 'ldapi://'
*/
public function usesLdapi(): bool {
return (substr($this->config['ldapHost'], 0, strlen('ldapi://')) === 'ldapi://');
}
}
@@ -0,0 +1,749 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jarkko Lehtoranta <devel@jlranta.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author root <root@localhost.localdomain>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Xuanwo <xuanwo@yunify.com>
* @author Vincent Van Houtte <vvh@aplusv.be>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OC\ServerNotAvailableException;
use Psr\Log\LoggerInterface;
/**
* magic properties (incomplete)
* responsible for LDAP connections in context with the provided configuration
*
* @property string ldapHost
* @property string ldapPort holds the port number
* @property string ldapUserFilter
* @property string ldapUserDisplayName
* @property string ldapUserDisplayName2
* @property string ldapUserAvatarRule
* @property boolean turnOnPasswordChange
* @property string[] ldapBaseUsers
* @property int|null ldapPagingSize holds an integer
* @property bool|mixed|void ldapGroupMemberAssocAttr
* @property string ldapUuidUserAttribute
* @property string ldapUuidGroupAttribute
* @property string ldapExpertUUIDUserAttr
* @property string ldapExpertUUIDGroupAttr
* @property string ldapQuotaAttribute
* @property string ldapQuotaDefault
* @property string ldapEmailAttribute
* @property string ldapExtStorageHomeAttribute
* @property string homeFolderNamingRule
* @property bool|string markRemnantsAsDisabled
* @property bool|string ldapNestedGroups
* @property string[] ldapBaseGroups
* @property string ldapGroupFilter
* @property string ldapGroupDisplayName
* @property string ldapLoginFilter
* @property string ldapDynamicGroupMemberURL
* @property string ldapGidNumber
* @property int hasMemberOfFilterSupport
* @property int useMemberOfToDetectMembership
* @property string ldapMatchingRuleInChainState
* @property string ldapAttributePhone
* @property string ldapAttributeWebsite
* @property string ldapAttributeAddress
* @property string ldapAttributeTwitter
* @property string ldapAttributeFediverse
* @property string ldapAttributeOrganisation
* @property string ldapAttributeRole
* @property string ldapAttributeHeadline
* @property string ldapAttributeBiography
* @property string ldapAdminGroup
*/
class Connection extends LDAPUtility {
/**
* @var resource|\LDAP\Connection|null
*/
private $ldapConnectionRes = null;
/**
* @var string
*/
private $configPrefix;
/**
* @var ?string
*/
private $configID;
/**
* @var bool
*/
private $configured = false;
/**
* @var bool whether connection should be kept on __destruct
*/
private $dontDestruct = false;
/**
* @var bool runtime flag that indicates whether supported primary groups are available
*/
public $hasPrimaryGroups = true;
/**
* @var bool runtime flag that indicates whether supported POSIX gidNumber are available
*/
public $hasGidNumber = true;
/**
* @var \OCP\ICache|null
*/
protected $cache = null;
/** @var Configuration settings handler **/
protected $configuration;
/**
* @var bool
*/
protected $doNotValidate = false;
/**
* @var bool
*/
protected $ignoreValidation = false;
/**
* @var array{sum?: string, result?: bool}
*/
protected $bindResult = [];
/** @var LoggerInterface */
protected $logger;
/**
* Constructor
* @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
* @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
*/
public function __construct(ILDAPWrapper $ldap, string $configPrefix = '', ?string $configID = 'user_ldap') {
parent::__construct($ldap);
$this->configPrefix = $configPrefix;
$this->configID = $configID;
$this->configuration = new Configuration($configPrefix, !is_null($configID));
$memcache = \OC::$server->getMemCacheFactory();
if ($memcache->isAvailable()) {
$this->cache = $memcache->createDistributed();
}
$helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
$this->doNotValidate = !in_array($this->configPrefix,
$helper->getServerConfigurationPrefixes());
$this->logger = \OC::$server->get(LoggerInterface::class);
}
public function __destruct() {
if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
@$this->ldap->unbind($this->ldapConnectionRes);
$this->bindResult = [];
}
}
/**
* defines behaviour when the instance is cloned
*/
public function __clone() {
$this->configuration = new Configuration($this->configPrefix,
!is_null($this->configID));
if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
$this->bindResult = [];
}
$this->ldapConnectionRes = null;
$this->dontDestruct = true;
}
public function __get(string $name) {
if (!$this->configured) {
$this->readConfiguration();
}
return $this->configuration->$name;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value) {
$this->doNotValidate = false;
$before = $this->configuration->$name;
$this->configuration->$name = $value;
$after = $this->configuration->$name;
if ($before !== $after) {
if ($this->configID !== '' && $this->configID !== null) {
$this->configuration->saveConfiguration();
}
$this->validateConfiguration();
}
}
/**
* @param string $rule
* @return array
* @throws \RuntimeException
*/
public function resolveRule($rule) {
return $this->configuration->resolveRule($rule);
}
/**
* sets whether the result of the configuration validation shall
* be ignored when establishing the connection. Used by the Wizard
* in early configuration state.
* @param bool $state
*/
public function setIgnoreValidation($state) {
$this->ignoreValidation = (bool)$state;
}
/**
* initializes the LDAP backend
* @param bool $force read the config settings no matter what
*/
public function init($force = false) {
$this->readConfiguration($force);
$this->establishConnection();
}
/**
* @return resource|\LDAP\Connection The LDAP resource
*/
public function getConnectionResource() {
if (!$this->ldapConnectionRes) {
$this->init();
} elseif (!$this->ldap->isResource($this->ldapConnectionRes)) {
$this->ldapConnectionRes = null;
$this->establishConnection();
}
if (is_null($this->ldapConnectionRes)) {
$this->logger->error(
'No LDAP Connection to server ' . $this->configuration->ldapHost,
['app' => 'user_ldap']
);
throw new ServerNotAvailableException('Connection to LDAP server could not be established');
}
return $this->ldapConnectionRes;
}
/**
* resets the connection resource
*/
public function resetConnectionResource() {
if (!is_null($this->ldapConnectionRes)) {
@$this->ldap->unbind($this->ldapConnectionRes);
$this->ldapConnectionRes = null;
$this->bindResult = [];
}
}
/**
* @param string|null $key
* @return string
*/
private function getCacheKey($key) {
$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
if (is_null($key)) {
return $prefix;
}
return $prefix.hash('sha256', $key);
}
/**
* @param string $key
* @return mixed|null
*/
public function getFromCache($key) {
if (!$this->configured) {
$this->readConfiguration();
}
if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
return null;
}
$key = $this->getCacheKey($key);
return json_decode(base64_decode($this->cache->get($key) ?? ''), true);
}
public function getConfigPrefix(): string {
return $this->configPrefix;
}
/**
* @param string $key
* @param mixed $value
*/
public function writeToCache($key, $value, int $ttlOverride = null): void {
if (!$this->configured) {
$this->readConfiguration();
}
if (is_null($this->cache)
|| !$this->configuration->ldapCacheTTL
|| !$this->configuration->ldapConfigurationActive) {
return;
}
$key = $this->getCacheKey($key);
$value = base64_encode(json_encode($value));
$ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL;
$this->cache->set($key, $value, $ttl);
}
public function clearCache() {
if (!is_null($this->cache)) {
$this->cache->clear($this->getCacheKey(null));
}
}
/**
* Caches the general LDAP configuration.
* @param bool $force optional. true, if the re-read should be forced. defaults
* to false.
* @return null
*/
private function readConfiguration($force = false) {
if ((!$this->configured || $force) && !is_null($this->configID)) {
$this->configuration->readConfiguration();
$this->configured = $this->validateConfiguration();
}
}
/**
* set LDAP configuration with values delivered by an array, not read from configuration
* @param array $config array that holds the config parameters in an associated array
* @param array &$setParameters optional; array where the set fields will be given to
* @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
*/
public function setConfiguration($config, &$setParameters = null): bool {
if (is_null($setParameters)) {
$setParameters = [];
}
$this->doNotValidate = false;
$this->configuration->setConfiguration($config, $setParameters);
if (count($setParameters) > 0) {
$this->configured = $this->validateConfiguration();
}
return $this->configured;
}
/**
* saves the current Configuration in the database and empties the
* cache
* @return null
*/
public function saveConfiguration() {
$this->configuration->saveConfiguration();
$this->clearCache();
}
/**
* get the current LDAP configuration
* @return array
*/
public function getConfiguration() {
$this->readConfiguration();
$config = $this->configuration->getConfiguration();
$cta = $this->configuration->getConfigTranslationArray();
$result = [];
foreach ($cta as $dbkey => $configkey) {
switch ($configkey) {
case 'homeFolderNamingRule':
if (str_starts_with($config[$configkey], 'attr:')) {
$result[$dbkey] = substr($config[$configkey], 5);
} else {
$result[$dbkey] = '';
}
break;
case 'ldapBase':
case 'ldapBaseUsers':
case 'ldapBaseGroups':
case 'ldapAttributesForUserSearch':
case 'ldapAttributesForGroupSearch':
if (is_array($config[$configkey])) {
$result[$dbkey] = implode("\n", $config[$configkey]);
break;
} //else follows default
// no break
default:
$result[$dbkey] = $config[$configkey];
}
}
return $result;
}
private function doSoftValidation() {
//if User or Group Base are not set, take over Base DN setting
foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
$val = $this->configuration->$keyBase;
if (empty($val)) {
$this->configuration->$keyBase = $this->configuration->ldapBase;
}
}
foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute']
as $expertSetting => $effectiveSetting) {
$uuidOverride = $this->configuration->$expertSetting;
if (!empty($uuidOverride)) {
$this->configuration->$effectiveSetting = $uuidOverride;
} else {
$uuidAttributes = Access::UUID_ATTRIBUTES;
array_unshift($uuidAttributes, 'auto');
if (!in_array($this->configuration->$effectiveSetting, $uuidAttributes)
&& !is_null($this->configID)) {
$this->configuration->$effectiveSetting = 'auto';
$this->configuration->saveConfiguration();
$this->logger->info(
'Illegal value for the '.$effectiveSetting.', reset to autodetect.',
['app' => 'user_ldap']
);
}
}
}
$backupPort = (int)$this->configuration->ldapBackupPort;
if ($backupPort <= 0) {
$this->configuration->backupPort = $this->configuration->ldapPort;
}
//make sure empty search attributes are saved as simple, empty array
$saKeys = ['ldapAttributesForUserSearch',
'ldapAttributesForGroupSearch'];
foreach ($saKeys as $key) {
$val = $this->configuration->$key;
if (is_array($val) && count($val) === 1 && empty($val[0])) {
$this->configuration->$key = [];
}
}
if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
&& $this->configuration->ldapTLS) {
$this->configuration->ldapTLS = false;
$this->logger->info(
'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
['app' => 'user_ldap']
);
}
}
/**
* @return bool
*/
private function doCriticalValidation() {
$configurationOK = true;
$errorStr = 'Configuration Error (prefix '.
(string)$this->configPrefix .'): ';
//options that shall not be empty
$options = ['ldapHost', 'ldapUserDisplayName',
'ldapGroupDisplayName', 'ldapLoginFilter'];
//ldapPort should not be empty either unless ldapHost is pointing to a socket
if (!$this->configuration->usesLdapi()) {
$options[] = 'ldapPort';
}
foreach ($options as $key) {
$val = $this->configuration->$key;
if (empty($val)) {
switch ($key) {
case 'ldapHost':
$subj = 'LDAP Host';
break;
case 'ldapPort':
$subj = 'LDAP Port';
break;
case 'ldapUserDisplayName':
$subj = 'LDAP User Display Name';
break;
case 'ldapGroupDisplayName':
$subj = 'LDAP Group Display Name';
break;
case 'ldapLoginFilter':
$subj = 'LDAP Login Filter';
break;
default:
$subj = $key;
break;
}
$configurationOK = false;
$this->logger->warning(
$errorStr.'No '.$subj.' given!',
['app' => 'user_ldap']
);
}
}
//combinations
$agent = $this->configuration->ldapAgentName;
$pwd = $this->configuration->ldapAgentPassword;
if (
($agent === '' && $pwd !== '')
|| ($agent !== '' && $pwd === '')
) {
$this->logger->warning(
$errorStr.'either no password is given for the user ' .
'agent or a password is given, but not an LDAP agent.',
['app' => 'user_ldap']
);
$configurationOK = false;
}
$base = $this->configuration->ldapBase;
$baseUsers = $this->configuration->ldapBaseUsers;
$baseGroups = $this->configuration->ldapBaseGroups;
if (empty($base) && empty($baseUsers) && empty($baseGroups)) {
$this->logger->warning(
$errorStr.'Not a single Base DN given.',
['app' => 'user_ldap']
);
$configurationOK = false;
}
if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
=== false) {
$this->logger->warning(
$errorStr.'login filter does not contain %uid place holder.',
['app' => 'user_ldap']
);
$configurationOK = false;
}
return $configurationOK;
}
/**
* Validates the user specified configuration
* @return bool true if configuration seems OK, false otherwise
*/
private function validateConfiguration() {
if ($this->doNotValidate) {
//don't do a validation if it is a new configuration with pure
//default values. Will be allowed on changes via __set or
//setConfiguration
return false;
}
// first step: "soft" checks: settings that are not really
// necessary, but advisable. If left empty, give an info message
$this->doSoftValidation();
//second step: critical checks. If left empty or filled wrong, mark as
//not configured and give a warning.
return $this->doCriticalValidation();
}
/**
* Connects and Binds to LDAP
*
* @throws ServerNotAvailableException
*/
private function establishConnection() {
if (!$this->configuration->ldapConfigurationActive) {
return null;
}
static $phpLDAPinstalled = true;
if (!$phpLDAPinstalled) {
return false;
}
if (!$this->ignoreValidation && !$this->configured) {
$this->logger->warning(
'Configuration is invalid, cannot connect',
['app' => 'user_ldap']
);
return false;
}
if (!$this->ldapConnectionRes) {
if (!$this->ldap->areLDAPFunctionsAvailable()) {
$phpLDAPinstalled = false;
$this->logger->error(
'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
['app' => 'user_ldap']
);
return false;
}
if ($this->configuration->turnOffCertCheck) {
if (putenv('LDAPTLS_REQCERT=never')) {
$this->logger->debug(
'Turned off SSL certificate validation successfully.',
['app' => 'user_ldap']
);
} else {
$this->logger->warning(
'Could not turn off SSL certificate validation.',
['app' => 'user_ldap']
);
}
}
$hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
$hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
$useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
$overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
$forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
$bindStatus = false;
if (!$forceBackupHost) {
try {
$host = $this->configuration->ldapHost ?? '';
$port = $this->configuration->ldapPort ?? '';
if ($useBackgroundHost) {
$host = $this->configuration->ldapBackgroundHost ?? '';
$port = $this->configuration->ldapBackgroundPort ?? '';
}
$this->doConnect($host, $port);
return $this->bind();
} catch (ServerNotAvailableException $e) {
if (!$hasBackupHost) {
throw $e;
}
}
$this->logger->warning(
'Main LDAP not reachable, connecting to backup',
[
'app' => 'user_ldap'
]
);
}
// if LDAP server is not reachable, try the Backup (Replica!) Server
$this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
$this->bindResult = [];
$bindStatus = $this->bind();
$error = $this->ldap->isResource($this->ldapConnectionRes) ?
$this->ldap->errno($this->ldapConnectionRes) : -1;
if ($bindStatus && $error === 0 && !$forceBackupHost) {
//when bind to backup server succeeded and failed to main server,
//skip contacting it for 15min
$this->writeToCache($overrideCacheKey, true, 60 * 15);
}
return $bindStatus;
}
return null;
}
/**
* @param string $host
* @param string $port
* @return bool
* @throws \OC\ServerNotAvailableException
*/
private function doConnect($host, $port) {
if ($host === '') {
return false;
}
$this->ldapConnectionRes = $this->ldap->connect($host, $port);
if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
}
if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
throw new ServerNotAvailableException('Could not disable LDAP referrals.');
}
if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
throw new ServerNotAvailableException('Could not set network timeout');
}
if ($this->configuration->ldapTLS) {
if (!$this->ldap->startTls($this->ldapConnectionRes)) {
throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
}
}
return true;
}
/**
* Binds to LDAP
*/
public function bind() {
if (!$this->configuration->ldapConfigurationActive) {
return false;
}
$cr = $this->ldapConnectionRes;
if (!$this->ldap->isResource($cr)) {
$cr = $this->getConnectionResource();
}
if (
count($this->bindResult) !== 0
&& $this->bindResult['sum'] === md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword)
) {
// don't attempt to bind again with the same data as before
// bind might have been invoked via getConnectionResource(),
// but we need results specifically for e.g. user login
return $this->bindResult['result'];
}
$ldapLogin = @$this->ldap->bind($cr,
$this->configuration->ldapAgentName,
$this->configuration->ldapAgentPassword);
$this->bindResult = [
'sum' => md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword),
'result' => $ldapLogin,
];
if (!$ldapLogin) {
$errno = $this->ldap->errno($cr);
$this->logger->warning(
'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
['app' => 'user_ldap']
);
// Set to failure mode, if LDAP error code is not one of
// - LDAP_SUCCESS (0)
// - LDAP_INVALID_CREDENTIALS (49)
// - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory)
// - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory)
if (!in_array($errno, [0, 49, 50, 53], true)) {
$this->ldapConnectionRes = null;
}
return false;
}
return true;
}
}
@@ -0,0 +1,36 @@
<?php
/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP;
class ConnectionFactory {
/** @var ILDAPWrapper */
private $ldap;
public function __construct(ILDAPWrapper $ldap) {
$this->ldap = $ldap;
}
public function get($prefix) {
return new Connection($this->ldap, $prefix, 'user_ldap');
}
}
@@ -0,0 +1,268 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author 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\User_LDAP\Controller;
use OC\CapabilitiesManager;
use OC\Core\Controller\OCSController;
use OC\Security\IdentityProof\Manager;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\ConnectionFactory;
use OCA\User_LDAP\Helper;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
class ConfigAPIController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
CapabilitiesManager $capabilitiesManager,
IUserSession $userSession,
IUserManager $userManager,
Manager $keyManager,
private Helper $ldapHelper,
private LoggerInterface $logger,
private ConnectionFactory $connectionFactory
) {
parent::__construct(
$appName,
$request,
$capabilitiesManager,
$userSession,
$userManager,
$keyManager
);
}
/**
* Create a new (empty) configuration and return the resulting prefix
*
* @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin)
* @return DataResponse<Http::STATUS_OK, array{configID: string}, array{}>
* @throws OCSException
*
* 200: Config created successfully
*/
public function create() {
try {
$configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix();
$configHolder = new Configuration($configPrefix);
$configHolder->ldapConfigurationActive = false;
$configHolder->saveConfiguration();
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new OCSException('An issue occurred when creating the new config.');
}
return new DataResponse(['configID' => $configPrefix]);
}
/**
* Delete a LDAP configuration
*
* @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin)
* @param string $configID ID of the config
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws OCSException
* @throws OCSNotFoundException Config not found
*
* 200: Config deleted successfully
*/
public function delete($configID) {
try {
$this->ensureConfigIDExists($configID);
if (!$this->ldapHelper->deleteServerConfiguration($configID)) {
throw new OCSException('Could not delete configuration');
}
} catch (OCSException $e) {
throw $e;
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new OCSException('An issue occurred when deleting the config.');
}
return new DataResponse();
}
/**
* Modify a configuration
*
* @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin)
* @param string $configID ID of the config
* @param array<string, mixed> $configData New config
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws OCSException
* @throws OCSBadRequestException Modifying config is not possible
* @throws OCSNotFoundException Config not found
*
* 200: Config returned
*/
public function modify($configID, $configData) {
try {
$this->ensureConfigIDExists($configID);
if (!is_array($configData)) {
throw new OCSBadRequestException('configData is not properly set');
}
$configuration = new Configuration($configID);
$configKeys = $configuration->getConfigTranslationArray();
foreach ($configKeys as $i => $key) {
if (isset($configData[$key])) {
$configuration->$key = $configData[$key];
}
}
$configuration->saveConfiguration();
$this->connectionFactory->get($configID)->clearCache();
} catch (OCSException $e) {
throw $e;
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new OCSException('An issue occurred when modifying the config.');
}
return new DataResponse();
}
/**
* Get a configuration
*
* Output can look like this:
* <?xml version="1.0"?>
* <ocs>
* <meta>
* <status>ok</status>
* <statuscode>200</statuscode>
* <message>OK</message>
* </meta>
* <data>
* <ldapHost>ldaps://my.ldap.server</ldapHost>
* <ldapPort>7770</ldapPort>
* <ldapBackupHost></ldapBackupHost>
* <ldapBackupPort></ldapBackupPort>
* <ldapBase>ou=small,dc=my,dc=ldap,dc=server</ldapBase>
* <ldapBaseUsers>ou=users,ou=small,dc=my,dc=ldap,dc=server</ldapBaseUsers>
* <ldapBaseGroups>ou=small,dc=my,dc=ldap,dc=server</ldapBaseGroups>
* <ldapAgentName>cn=root,dc=my,dc=ldap,dc=server</ldapAgentName>
* <ldapAgentPassword>clearTextWithShowPassword=1</ldapAgentPassword>
* <ldapTLS>1</ldapTLS>
* <turnOffCertCheck>0</turnOffCertCheck>
* <ldapIgnoreNamingRules/>
* <ldapUserDisplayName>displayname</ldapUserDisplayName>
* <ldapUserDisplayName2>uid</ldapUserDisplayName2>
* <ldapUserFilterObjectclass>inetOrgPerson</ldapUserFilterObjectclass>
* <ldapUserFilterGroups></ldapUserFilterGroups>
* <ldapUserFilter>(&amp;(objectclass=nextcloudUser)(nextcloudEnabled=TRUE))</ldapUserFilter>
* <ldapUserFilterMode>1</ldapUserFilterMode>
* <ldapGroupFilter>(&amp;(|(objectclass=nextcloudGroup)))</ldapGroupFilter>
* <ldapGroupFilterMode>0</ldapGroupFilterMode>
* <ldapGroupFilterObjectclass>nextcloudGroup</ldapGroupFilterObjectclass>
* <ldapGroupFilterGroups></ldapGroupFilterGroups>
* <ldapGroupDisplayName>cn</ldapGroupDisplayName>
* <ldapGroupMemberAssocAttr>memberUid</ldapGroupMemberAssocAttr>
* <ldapLoginFilter>(&amp;(|(objectclass=inetOrgPerson))(uid=%uid))</ldapLoginFilter>
* <ldapLoginFilterMode>0</ldapLoginFilterMode>
* <ldapLoginFilterEmail>0</ldapLoginFilterEmail>
* <ldapLoginFilterUsername>1</ldapLoginFilterUsername>
* <ldapLoginFilterAttributes></ldapLoginFilterAttributes>
* <ldapQuotaAttribute></ldapQuotaAttribute>
* <ldapQuotaDefault></ldapQuotaDefault>
* <ldapEmailAttribute>mail</ldapEmailAttribute>
* <ldapCacheTTL>20</ldapCacheTTL>
* <ldapUuidUserAttribute>auto</ldapUuidUserAttribute>
* <ldapUuidGroupAttribute>auto</ldapUuidGroupAttribute>
* <ldapOverrideMainServer></ldapOverrideMainServer>
* <ldapConfigurationActive>1</ldapConfigurationActive>
* <ldapAttributesForUserSearch>uid;sn;givenname</ldapAttributesForUserSearch>
* <ldapAttributesForGroupSearch></ldapAttributesForGroupSearch>
* <ldapExperiencedAdmin>0</ldapExperiencedAdmin>
* <homeFolderNamingRule></homeFolderNamingRule>
* <hasMemberOfFilterSupport></hasMemberOfFilterSupport>
* <useMemberOfToDetectMembership>1</useMemberOfToDetectMembership>
* <ldapExpertUsernameAttr>uid</ldapExpertUsernameAttr>
* <ldapExpertUUIDUserAttr>uid</ldapExpertUUIDUserAttr>
* <ldapExpertUUIDGroupAttr></ldapExpertUUIDGroupAttr>
* <lastJpegPhotoLookup>0</lastJpegPhotoLookup>
* <ldapNestedGroups>0</ldapNestedGroups>
* <ldapPagingSize>500</ldapPagingSize>
* <turnOnPasswordChange>1</turnOnPasswordChange>
* <ldapDynamicGroupMemberURL></ldapDynamicGroupMemberURL>
* </data>
* </ocs>
*
* @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin)
* @param string $configID ID of the config
* @param bool $showPassword Whether to show the password
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>
* @throws OCSException
* @throws OCSNotFoundException Config not found
*
* 200: Config returned
*/
public function show($configID, $showPassword = false) {
try {
$this->ensureConfigIDExists($configID);
$config = new Configuration($configID);
$data = $config->getConfiguration();
if (!$showPassword) {
$data['ldapAgentPassword'] = '***';
}
foreach ($data as $key => $value) {
if (is_array($value)) {
$value = implode(';', $value);
$data[$key] = $value;
}
}
} catch (OCSException $e) {
throw $e;
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw new OCSException('An issue occurred when modifying the config.');
}
return new DataResponse($data);
}
/**
* If the given config ID is not available, an exception is thrown
*
* @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin)
* @param string $configID
* @throws OCSNotFoundException
*/
private function ensureConfigIDExists($configID): void {
$prefixes = $this->ldapHelper->getServerConfigurationPrefixes();
if (!in_array($configID, $prefixes, true)) {
throw new OCSNotFoundException('Config ID not found');
}
}
}
@@ -0,0 +1,180 @@
<?php
/**
* @copyright Copyright (c) 2017 Roger Szabo <roger.szabo@web.de>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roger Szabo <roger.szabo@web.de>
*
* @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\User_LDAP\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\HintException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class RenewPasswordController extends Controller {
/** @var IUserManager */
private $userManager;
/** @var IConfig */
private $config;
/** @var IL10N */
protected $l10n;
/** @var ISession */
private $session;
/** @var IURLGenerator */
private $urlGenerator;
/**
* @param string $appName
* @param IRequest $request
* @param IUserManager $userManager
* @param IConfig $config
* @param IURLGenerator $urlGenerator
*/
public function __construct($appName, IRequest $request, IUserManager $userManager,
IConfig $config, IL10N $l10n, ISession $session, IURLGenerator $urlGenerator) {
parent::__construct($appName, $request);
$this->userManager = $userManager;
$this->config = $config;
$this->l10n = $l10n;
$this->session = $session;
$this->urlGenerator = $urlGenerator;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return RedirectResponse
*/
public function cancel() {
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm'));
}
/**
* @PublicPage
* @NoCSRFRequired
* @UseSession
*
* @param string $user
*
* @return TemplateResponse|RedirectResponse
*/
public function showRenewPasswordForm($user) {
if ($this->config->getUserValue($user, 'user_ldap', 'needsPasswordReset') !== 'true') {
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm'));
}
$parameters = [];
$renewPasswordMessages = $this->session->get('renewPasswordMessages');
$errors = [];
$messages = [];
if (is_array($renewPasswordMessages)) {
[$errors, $messages] = $renewPasswordMessages;
}
$this->session->remove('renewPasswordMessages');
foreach ($errors as $value) {
$parameters[$value] = true;
}
$parameters['messages'] = $messages;
$parameters['user'] = $user;
$parameters['canResetPassword'] = true;
$parameters['resetPasswordLink'] = $this->config->getSystemValue('lost_password_link', '');
if (!$parameters['resetPasswordLink']) {
$userObj = $this->userManager->get($user);
if ($userObj instanceof IUser) {
$parameters['canResetPassword'] = $userObj->canChangePassword();
}
}
$parameters['cancelLink'] = $this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm');
return new TemplateResponse(
$this->appName, 'renewpassword', $parameters, 'guest'
);
}
/**
* @PublicPage
* @UseSession
*
* @param string $user
* @param string $oldPassword
* @param string $newPassword
*
* @return RedirectResponse
*/
public function tryRenewPassword($user, $oldPassword, $newPassword) {
if ($this->config->getUserValue($user, 'user_ldap', 'needsPasswordReset') !== 'true') {
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm'));
}
$args = !is_null($user) ? ['user' => $user] : [];
$loginResult = $this->userManager->checkPassword($user, $oldPassword);
if ($loginResult === false) {
$this->session->set('renewPasswordMessages', [
['invalidpassword'], []
]);
return new RedirectResponse($this->urlGenerator->linkToRoute('user_ldap.renewPassword.showRenewPasswordForm', $args));
}
try {
if (!is_null($newPassword) && \OC_User::setPassword($user, $newPassword)) {
$this->session->set('loginMessages', [
[], [$this->l10n->t("Please login with the new password")]
]);
$this->config->setUserValue($user, 'user_ldap', 'needsPasswordReset', 'false');
return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args));
} else {
$this->session->set('renewPasswordMessages', [
['internalexception'], []
]);
}
} catch (HintException $e) {
$this->session->set('renewPasswordMessages', [
[], [$e->getHint()]
]);
}
return new RedirectResponse($this->urlGenerator->linkToRoute('user_ldap.renewPassword.showRenewPasswordForm', $args));
}
/**
* @PublicPage
* @NoCSRFRequired
* @UseSession
*
* @return RedirectResponse
*/
public function showLoginFormInvalidPassword($user) {
$args = !is_null($user) ? ['user' => $user] : [];
$this->session->set('loginMessages', [
['invalidpassword'], []
]);
return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args));
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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\User_LDAP\DataCollector;
use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
use OCP\DataCollector\AbstractDataCollector;
class LdapDataCollector extends AbstractDataCollector {
public function startLdapRequest(string $query, array $args, array $backtrace): void {
$this->data[] = [
'start' => microtime(true),
'query' => $query,
'args' => $args,
'end' => microtime(true),
'backtrace' => $backtrace,
];
}
public function stopLastLdapRequest(): void {
$this->data[count($this->data) - 1]['end'] = microtime(true);
}
public function getName(): string {
return 'ldap';
}
public function collect(Request $request, Response $response, \Throwable $exception = null): void {
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Côme Chilliet <come.chilliet@nextcloud.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Db;
use OCP\AppFramework\Db\Entity;
/**
* @method void setUserid(string $userid)
* @method string getUserid()
* @method void setGroupid(string $groupid)
* @method string getGroupid()
*/
class GroupMembership extends Entity {
/** @var string */
protected $groupid;
/** @var string */
protected $userid;
public function __construct() {
$this->addType('groupid', 'string');
$this->addType('userid', 'string');
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Côme Chilliet <come.chilliet@nextcloud.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Db;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<GroupMembership>
*/
class GroupMembershipMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'ldap_group_membership', GroupMembership::class);
}
/**
* @return string[]
*/
public function getKnownGroups(): array {
$query = $this->db->getQueryBuilder();
$result = $query->selectDistinct('groupid')
->from($this->getTableName())
->executeQuery();
$groups = array_column($result->fetchAll(), 'groupid');
$result->closeCursor();
return $groups;
}
/**
* @return GroupMembership[]
*/
public function findGroupMemberships(string $groupid): array {
$qb = $this->db->getQueryBuilder();
$select = $qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('groupid', $qb->createNamedParameter($groupid)));
return $this->findEntities($select);
}
/**
* @return GroupMembership[]
*/
public function findGroupMembershipsForUser(string $userid): array {
$qb = $this->db->getQueryBuilder();
$select = $qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('userid', $qb->createNamedParameter($userid)));
return $this->findEntities($select);
}
public function deleteGroups(array $removedGroups): void {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->in('groupid', $query->createParameter('groupids')));
foreach (array_chunk($removedGroups, 1000) as $removedGroupsChunk) {
$query->setParameter('groupids', $removedGroupsChunk, IQueryBuilder::PARAM_STR_ARRAY);
$query->executeStatement();
}
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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\User_LDAP\Events;
use OCA\User_LDAP\GroupPluginManager;
use OCA\User_LDAP\IGroupLDAP;
use OCP\EventDispatcher\Event;
/**
* This event is triggered right after the LDAP group backend is registered.
*
* @since 20.0.0
*/
class GroupBackendRegistered extends Event {
/** @var GroupPluginManager */
private $pluginManager;
/** @var IGroupLDAP */
private $backend;
public function __construct(IGroupLDAP $backend, GroupPluginManager $pluginManager) {
$this->pluginManager = $pluginManager;
$this->backend = $backend;
}
public function getBackend(): IGroupLDAP {
return $this->backend;
}
public function getPluginManager(): GroupPluginManager {
return $this->pluginManager;
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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\User_LDAP\Events;
use OCA\User_LDAP\IUserLDAP;
use OCA\User_LDAP\UserPluginManager;
use OCP\EventDispatcher\Event;
/**
* This event is triggered right after the LDAP user backend is registered.
*
* @since 20.0.0
*/
class UserBackendRegistered extends Event {
/** @var IUserLDAP */
private $backend;
/** @var UserPluginManager */
private $pluginManager;
public function __construct(IUserLDAP $backend, UserPluginManager $pluginManager) {
$this->backend = $backend;
$this->pluginManager = $pluginManager;
}
public function getBackend(): IUserLDAP {
return $this->backend;
}
public function getPluginManager(): UserPluginManager {
return $this->pluginManager;
}
}
@@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author 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\User_LDAP\Exceptions;
class AttributeNotSet extends \RuntimeException {
}
@@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2017 Roger Szabo <roger.szabo@web.de>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roger Szabo <roger.szabo@web.de>
*
* @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\User_LDAP\Exceptions;
class ConstraintViolationException extends \Exception {
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Exceptions;
class NoMoreResults extends \Exception {
}
@@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author 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\User_LDAP\Exceptions;
class NotOnLDAP extends \Exception {
}
@@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
/**
* @brief wraps around static Nextcloud core methods
*/
class FilesystemHelper {
/**
* @brief states whether the filesystem was loaded
* @return bool
*/
public function isLoaded() {
return \OC\Files\Filesystem::$loaded;
}
/**
* @brief initializes the filesystem for the given user
* @param string $uid the Nextcloud username of the user
*/
public function setup($uid) {
\OC_Util::setupFS($uid);
}
}
@@ -0,0 +1,186 @@
<?php
/**
* @copyright Copyright (c) 2017 EITA Cooperative (eita.org.br)
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
use OCP\GroupInterface;
class GroupPluginManager {
private int $respondToActions = 0;
/** @var array<int, ?ILDAPGroupPlugin> */
private array $which = [
GroupInterface::CREATE_GROUP => null,
GroupInterface::DELETE_GROUP => null,
GroupInterface::ADD_TO_GROUP => null,
GroupInterface::REMOVE_FROM_GROUP => null,
GroupInterface::COUNT_USERS => null,
GroupInterface::GROUP_DETAILS => null
];
private bool $suppressDeletion = false;
/**
* @return int All implemented actions
*/
public function getImplementedActions() {
return $this->respondToActions;
}
/**
* Registers a group plugin that may implement some actions, overriding User_LDAP's group actions.
* @param ILDAPGroupPlugin $plugin
*/
public function register(ILDAPGroupPlugin $plugin) {
$respondToActions = $plugin->respondToActions();
$this->respondToActions |= $respondToActions;
foreach ($this->which as $action => $v) {
if ((bool)($respondToActions & $action)) {
$this->which[$action] = $plugin;
\OC::$server->getLogger()->debug("Registered action ".$action." to plugin ".get_class($plugin), ['app' => 'user_ldap']);
}
}
}
/**
* Signal if there is a registered plugin that implements some given actions
* @param int $actions Actions defined in \OCP\GroupInterface, like GroupInterface::REMOVE_FROM_GROUP
* @return bool
*/
public function implementsActions($actions) {
return ($actions & $this->respondToActions) == $actions;
}
/**
* Create a group
* @param string $gid Group Id
* @return string | null The group DN if group creation was successful.
* @throws \Exception
*/
public function createGroup($gid) {
$plugin = $this->which[GroupInterface::CREATE_GROUP];
if ($plugin) {
return $plugin->createGroup($gid);
}
throw new \Exception('No plugin implements createGroup in this LDAP Backend.');
}
public function canDeleteGroup(): bool {
return !$this->suppressDeletion && $this->implementsActions(GroupInterface::DELETE_GROUP);
}
/**
* @return bool – the value before the change
*/
public function setSuppressDeletion(bool $value): bool {
$old = $this->suppressDeletion;
$this->suppressDeletion = $value;
return $old;
}
/**
* Delete a group
*
* @throws \Exception
*/
public function deleteGroup(string $gid): bool {
$plugin = $this->which[GroupInterface::DELETE_GROUP];
if ($plugin) {
if ($this->suppressDeletion) {
return false;
}
return $plugin->deleteGroup($gid);
}
throw new \Exception('No plugin implements deleteGroup in this LDAP Backend.');
}
/**
* Add a user to a group
* @param string $uid ID of the user to add to group
* @param string $gid ID of the group in which add the user
* @return bool
* @throws \Exception
*
* Adds a user to a group.
*/
public function addToGroup($uid, $gid) {
$plugin = $this->which[GroupInterface::ADD_TO_GROUP];
if ($plugin) {
return $plugin->addToGroup($uid, $gid);
}
throw new \Exception('No plugin implements addToGroup in this LDAP Backend.');
}
/**
* Removes a user from a group
* @param string $uid ID of the user to remove from group
* @param string $gid ID of the group from which remove the user
* @return bool
* @throws \Exception
*
* removes the user from a group.
*/
public function removeFromGroup($uid, $gid) {
$plugin = $this->which[GroupInterface::REMOVE_FROM_GROUP];
if ($plugin) {
return $plugin->removeFromGroup($uid, $gid);
}
throw new \Exception('No plugin implements removeFromGroup in this LDAP Backend.');
}
/**
* get the number of all users matching the search string in a group
* @param string $gid ID of the group
* @param string $search query string
* @return int|false
* @throws \Exception
*/
public function countUsersInGroup($gid, $search = '') {
$plugin = $this->which[GroupInterface::COUNT_USERS];
if ($plugin) {
return $plugin->countUsersInGroup($gid, $search);
}
throw new \Exception('No plugin implements countUsersInGroup in this LDAP Backend.');
}
/**
* get an array with group details
* @param string $gid
* @return array|false
* @throws \Exception
*/
public function getGroupDetails($gid) {
$plugin = $this->which[GroupInterface::GROUP_DETAILS];
if ($plugin) {
return $plugin->getGroupDetails($gid);
}
throw new \Exception('No plugin implements getGroupDetails in this LDAP Backend.');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,404 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Johannes Leuker <j.leuker@hosting.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OC\ServerNotAvailableException;
use OCP\Group\Backend\IBatchMethodsBackend;
use OCP\Group\Backend\IDeleteGroupBackend;
use OCP\Group\Backend\IGetDisplayNameBackend;
use OCP\Group\Backend\IGroupDetailsBackend;
use OCP\Group\Backend\IIsAdminBackend;
use OCP\Group\Backend\INamedBackend;
use OCP\GroupInterface;
use OCP\IConfig;
use OCP\IUserManager;
class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend, IBatchMethodsBackend, IIsAdminBackend {
private $backends = [];
private ?Group_LDAP $refBackend = null;
private Helper $helper;
private GroupPluginManager $groupPluginManager;
private bool $isSetUp = false;
private IConfig $config;
private IUserManager $ncUserManager;
public function __construct(
Helper $helper,
ILDAPWrapper $ldap,
AccessFactory $accessFactory,
GroupPluginManager $groupPluginManager,
IConfig $config,
IUserManager $ncUserManager,
) {
parent::__construct($ldap, $accessFactory);
$this->helper = $helper;
$this->groupPluginManager = $groupPluginManager;
$this->config = $config;
$this->ncUserManager = $ncUserManager;
}
protected function setup(): void {
if ($this->isSetUp) {
return;
}
$serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true);
foreach ($serverConfigPrefixes as $configPrefix) {
$this->backends[$configPrefix] =
new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager);
if (is_null($this->refBackend)) {
$this->refBackend = &$this->backends[$configPrefix];
}
}
$this->isSetUp = true;
}
/**
* Tries the backends one after the other until a positive result is returned from the specified method
*
* @param string $id the gid connected to the request
* @param string $method the method of the group backend that shall be called
* @param array $parameters an array of parameters to be passed
* @return mixed the result of the method or false
*/
protected function walkBackends($id, $method, $parameters) {
$this->setup();
$gid = $id;
$cacheKey = $this->getGroupCacheKey($gid);
foreach ($this->backends as $configPrefix => $backend) {
if ($result = call_user_func_array([$backend, $method], $parameters)) {
if (!$this->isSingleBackend()) {
$this->writeToCache($cacheKey, $configPrefix);
}
return $result;
}
}
return false;
}
/**
* Asks the backend connected to the server that supposely takes care of the gid from the request.
*
* @param string $id the gid connected to the request
* @param string $method the method of the group backend that shall be called
* @param array $parameters an array of parameters to be passed
* @param mixed $passOnWhen the result matches this variable
* @return mixed the result of the method or false
*/
protected function callOnLastSeenOn($id, $method, $parameters, $passOnWhen) {
$this->setup();
$gid = $id;
$cacheKey = $this->getGroupCacheKey($gid);
$prefix = $this->getFromCache($cacheKey);
//in case the uid has been found in the past, try this stored connection first
if (!is_null($prefix)) {
if (isset($this->backends[$prefix])) {
$result = call_user_func_array([$this->backends[$prefix], $method], $parameters);
if ($result === $passOnWhen) {
//not found here, reset cache to null if group vanished
//because sometimes methods return false with a reason
$groupExists = call_user_func_array(
[$this->backends[$prefix], 'groupExists'],
[$gid]
);
if (!$groupExists) {
$this->writeToCache($cacheKey, null);
}
}
return $result;
}
}
return false;
}
protected function activeBackends(): int {
$this->setup();
return count($this->backends);
}
/**
* is user in group?
*
* @param string $uid uid of the user
* @param string $gid gid of the group
* @return bool
*
* Checks whether the user is member of a group or not.
*/
public function inGroup($uid, $gid) {
return $this->handleRequest($gid, 'inGroup', [$uid, $gid]);
}
/**
* Get all groups a user belongs to
*
* @param string $uid Name of the user
* @return string[] with group names
*
* This function fetches all groups a user belongs to. It does not check
* if the user exists at all.
*/
public function getUserGroups($uid) {
$this->setup();
$groups = [];
foreach ($this->backends as $backend) {
$backendGroups = $backend->getUserGroups($uid);
if (is_array($backendGroups)) {
$groups = array_merge($groups, $backendGroups);
}
}
return array_values(array_unique($groups));
}
/**
* get a list of all users in a group
*
* @return array<int,string> user ids
*/
public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
$this->setup();
$users = [];
foreach ($this->backends as $backend) {
$backendUsers = $backend->usersInGroup($gid, $search, $limit, $offset);
if (is_array($backendUsers)) {
$users = array_merge($users, $backendUsers);
}
}
return $users;
}
/**
* @param string $gid
* @return bool
*/
public function createGroup($gid) {
return $this->handleRequest(
$gid, 'createGroup', [$gid]);
}
/**
* delete a group
*/
public function deleteGroup(string $gid): bool {
return $this->handleRequest(
$gid, 'deleteGroup', [$gid]);
}
/**
* Add a user to a group
*
* @param string $uid Name of the user to add to group
* @param string $gid Name of the group in which add the user
* @return bool
*
* Adds a user to a group.
*/
public function addToGroup($uid, $gid) {
return $this->handleRequest(
$gid, 'addToGroup', [$uid, $gid]);
}
/**
* Removes a user from a group
*
* @param string $uid Name of the user to remove from group
* @param string $gid Name of the group from which remove the user
* @return bool
*
* removes the user from a group.
*/
public function removeFromGroup($uid, $gid) {
return $this->handleRequest(
$gid, 'removeFromGroup', [$uid, $gid]);
}
/**
* returns the number of users in a group, who match the search term
*
* @param string $gid the internal group name
* @param string $search optional, a search string
* @return int|bool
*/
public function countUsersInGroup($gid, $search = '') {
return $this->handleRequest(
$gid, 'countUsersInGroup', [$gid, $search]);
}
/**
* get an array with group details
*
* @param string $gid
* @return array|false
*/
public function getGroupDetails($gid) {
return $this->handleRequest(
$gid, 'getGroupDetails', [$gid]);
}
/**
* {@inheritdoc}
*/
public function getGroupsDetails(array $gids): array {
if (!($this instanceof IGroupDetailsBackend || $this->implementsActions(GroupInterface::GROUP_DETAILS))) {
throw new \Exception("Should not have been called");
}
$groupData = [];
foreach ($gids as $gid) {
$groupData[$gid] = $this->handleRequest($gid, 'getGroupDetails', [$gid]);
}
return $groupData;
}
/**
* get a list of all groups
*
* @return string[] with group names
*
* Returns a list with all groups
*/
public function getGroups($search = '', $limit = -1, $offset = 0) {
$this->setup();
$groups = [];
foreach ($this->backends as $backend) {
$backendGroups = $backend->getGroups($search, $limit, $offset);
if (is_array($backendGroups)) {
$groups = array_merge($groups, $backendGroups);
}
}
return $groups;
}
/**
* check if a group exists
*
* @param string $gid
* @return bool
*/
public function groupExists($gid) {
return $this->handleRequest($gid, 'groupExists', [$gid]);
}
/**
* Check if a group exists
*
* @throws ServerNotAvailableException
*/
public function groupExistsOnLDAP(string $gid, bool $ignoreCache = false): bool {
return $this->handleRequest($gid, 'groupExistsOnLDAP', [$gid, $ignoreCache]);
}
/**
* returns the groupname for the given LDAP DN, if available
*/
public function dn2GroupName(string $dn): string|false {
$id = 'DN,' . $dn;
return $this->handleRequest($id, 'dn2GroupName', [$dn]);
}
/**
* {@inheritdoc}
*/
public function groupsExists(array $gids): array {
return array_values(array_filter(
$gids,
fn (string $gid): bool => $this->handleRequest($gid, 'groupExists', [$gid]),
));
}
/**
* Check if backend implements actions
*
* @param int $actions bitwise-or'ed actions
* @return boolean
*
* Returns the supported actions as int to be
* compared with \OCP\GroupInterface::CREATE_GROUP etc.
*/
public function implementsActions($actions) {
$this->setup();
//it's the same across all our user backends obviously
return $this->refBackend->implementsActions($actions);
}
/**
* Return access for LDAP interaction.
*
* @param string $gid
* @return Access instance of Access for LDAP interaction
*/
public function getLDAPAccess($gid) {
return $this->handleRequest($gid, 'getLDAPAccess', [$gid]);
}
/**
* Return a new LDAP connection for the specified group.
* The connection needs to be closed manually.
*
* @param string $gid
* @return resource|\LDAP\Connection The LDAP connection
*/
public function getNewLDAPConnection($gid) {
return $this->handleRequest($gid, 'getNewLDAPConnection', [$gid]);
}
public function getDisplayName(string $gid): string {
return $this->handleRequest($gid, 'getDisplayName', [$gid]);
}
/**
* Backend name to be shown in group management
* @return string the name of the backend to be shown
* @since 22.0.0
*/
public function getBackendName(): string {
return 'LDAP';
}
public function searchInGroup(string $gid, string $search = '', int $limit = -1, int $offset = 0): array {
return $this->handleRequest($gid, 'searchInGroup', [$gid, $search, $limit, $offset]);
}
public function addRelationshipToCaches(string $uid, ?string $dnUser, string $gid): void {
$this->handleRequest($gid, 'addRelationshipToCaches', [$uid, $dnUser, $gid]);
}
public function isAdmin(string $uid): bool {
return $this->handleRequest($uid, 'isAdmin', [$uid]);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
*
* @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\User_LDAP\Handler;
use OCA\Files_External\Config\IConfigHandler;
use OCA\Files_External\Config\SimpleSubstitutionTrait;
use OCA\Files_External\Config\UserContext;
use OCA\User_LDAP\User_Proxy;
class ExtStorageConfigHandler extends UserContext implements IConfigHandler {
use SimpleSubstitutionTrait;
/**
* @param mixed $optionValue
* @return mixed the same type as $optionValue
* @since 16.0.0
* @throws \Exception
*/
public function handle($optionValue) {
$this->placeholder = 'home';
$user = $this->getUser();
if ($user === null) {
return $optionValue;
}
$backend = $user->getBackend();
if (!$backend instanceof User_Proxy) {
return $optionValue;
}
$access = $backend->getLDAPAccess($user->getUID());
if (!$access) {
return $optionValue;
}
$attribute = $access->connection->ldapExtStorageHomeAttribute;
if (empty($attribute)) {
return $optionValue;
}
$ldapUser = $access->userManager->get($user->getUID());
$extHome = $ldapUser !== null ? $ldapUser->getExtStorageHome() : '';
return $this->processInput($optionValue, $extHome);
}
}
+287
View File
@@ -0,0 +1,287 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author root <root@localhost.localdomain>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OCP\Cache\CappedMemoryCache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
class Helper {
private IConfig $config;
private IDBConnection $connection;
/** @var CappedMemoryCache<string> */
protected CappedMemoryCache $sanitizeDnCache;
public function __construct(IConfig $config,
IDBConnection $connection) {
$this->config = $config;
$this->connection = $connection;
$this->sanitizeDnCache = new CappedMemoryCache(10000);
}
/**
* returns prefixes for each saved LDAP/AD server configuration.
*
* @param bool $activeConfigurations optional, whether only active configuration shall be
* retrieved, defaults to false
* @return array with a list of the available prefixes
*
* Configuration prefixes are used to set up configurations for n LDAP or
* AD servers. Since configuration is stored in the database, table
* appconfig under appid user_ldap, the common identifiers in column
* 'configkey' have a prefix. The prefix for the very first server
* configuration is empty.
* Configkey Examples:
* Server 1: ldap_login_filter
* Server 2: s1_ldap_login_filter
* Server 3: s2_ldap_login_filter
*
* The prefix needs to be passed to the constructor of Connection class,
* except the default (first) server shall be connected to.
*
*/
public function getServerConfigurationPrefixes($activeConfigurations = false): array {
$referenceConfigkey = 'ldap_configuration_active';
$keys = $this->getServersConfig($referenceConfigkey);
$prefixes = [];
foreach ($keys as $key) {
if ($activeConfigurations && $this->config->getAppValue('user_ldap', $key, '0') !== '1') {
continue;
}
$len = strlen($key) - strlen($referenceConfigkey);
$prefixes[] = substr($key, 0, $len);
}
asort($prefixes);
return $prefixes;
}
/**
*
* determines the host for every configured connection
*
* @return array an array with configprefix as keys
*
*/
public function getServerConfigurationHosts() {
$referenceConfigkey = 'ldap_host';
$keys = $this->getServersConfig($referenceConfigkey);
$result = [];
foreach ($keys as $key) {
$len = strlen($key) - strlen($referenceConfigkey);
$prefix = substr($key, 0, $len);
$result[$prefix] = $this->config->getAppValue('user_ldap', $key);
}
return $result;
}
/**
* return the next available configuration prefix
*
* @return string
*/
public function getNextServerConfigurationPrefix() {
$serverConnections = $this->getServerConfigurationPrefixes();
if (count($serverConnections) === 0) {
return 's01';
}
sort($serverConnections);
$lastKey = array_pop($serverConnections);
$lastNumber = (int)str_replace('s', '', $lastKey);
return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
}
private function getServersConfig(string $value): array {
$regex = '/' . $value . '$/S';
$keys = $this->config->getAppKeys('user_ldap');
$result = [];
foreach ($keys as $key) {
if (preg_match($regex, $key) === 1) {
$result[] = $key;
}
}
return $result;
}
/**
* deletes a given saved LDAP/AD server configuration.
*
* @param string $prefix the configuration prefix of the config to delete
* @return bool true on success, false otherwise
*/
public function deleteServerConfiguration($prefix) {
if (!in_array($prefix, self::getServerConfigurationPrefixes())) {
return false;
}
$query = $this->connection->getQueryBuilder();
$query->delete('appconfig')
->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
'enabled',
'installed_version',
'types',
'bgjUpdateGroupsLastRun',
], IQueryBuilder::PARAM_STR_ARRAY)));
if (empty($prefix)) {
$query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
}
$deletedRows = $query->execute();
return $deletedRows !== 0;
}
/**
* checks whether there is one or more disabled LDAP configurations
*/
public function haveDisabledConfigurations(): bool {
$all = $this->getServerConfigurationPrefixes(false);
$active = $this->getServerConfigurationPrefixes(true);
return count($all) !== count($active) || count($all) === 0;
}
/**
* extracts the domain from a given URL
*
* @param string $url the URL
* @return string|false domain as string on success, false otherwise
*/
public function getDomainFromURL($url) {
$uinfo = parse_url($url);
if (!is_array($uinfo)) {
return false;
}
$domain = false;
if (isset($uinfo['host'])) {
$domain = $uinfo['host'];
} elseif (isset($uinfo['path'])) {
$domain = $uinfo['path'];
}
return $domain;
}
/**
* sanitizes a DN received from the LDAP server
*
* @param array|string $dn the DN in question
* @return array|string the sanitized DN
*/
public function sanitizeDN($dn) {
//treating multiple base DNs
if (is_array($dn)) {
$result = [];
foreach ($dn as $singleDN) {
$result[] = $this->sanitizeDN($singleDN);
}
return $result;
}
if (!is_string($dn)) {
throw new \LogicException('String expected ' . \gettype($dn) . ' given');
}
if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
return $sanitizedDn;
}
//OID sometimes gives back DNs with whitespace after the comma
// a la "uid=foo, cn=bar, dn=..." We need to tackle this!
$sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
//make comparisons and everything work
$sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
//escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
//to use the DN in search filters, \ needs to be escaped to \5c additionally
//to use them in bases, we convert them back to simple backslashes in readAttribute()
$replacements = [
'\,' => '\5c2C',
'\=' => '\5c3D',
'\+' => '\5c2B',
'\<' => '\5c3C',
'\>' => '\5c3E',
'\;' => '\5c3B',
'\"' => '\5c22',
'\#' => '\5c23',
'(' => '\28',
')' => '\29',
'*' => '\2A',
];
$sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
$this->sanitizeDnCache->set($dn, $sanitizedDn);
return $sanitizedDn;
}
/**
* converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
*
* @param string $dn the DN
* @return string
*/
public function DNasBaseParameter($dn) {
return str_ireplace('\\5c', '\\', $dn);
}
/**
* listens to a hook thrown by server2server sharing and replaces the given
* login name by a username, if it matches an LDAP user.
*
* @param array $param contains a reference to a $uid var under 'uid' key
* @throws \Exception
*/
public static function loginName2UserName($param): void {
if (!isset($param['uid'])) {
throw new \Exception('key uid is expected to be set in $param');
}
$userBackend = \OC::$server->get(User_Proxy::class);
$uid = $userBackend->loginName2UserName($param['uid']);
if ($uid !== false) {
$param['uid'] = $uid;
}
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (c) 2017, EITA Cooperative (eita.org.br)
*
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
interface IGroupLDAP {
//Used by LDAPProvider
/**
* Return access for LDAP interaction.
* @param string $gid
* @return Access instance of Access for LDAP interaction
*/
public function getLDAPAccess($gid);
/**
* Return a new LDAP connection for the specified group.
* @param string $gid
* @return resource|\LDAP\Connection The LDAP connection
*/
public function getNewLDAPConnection($gid);
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright Copyright (c) 2017 EITA Cooperative (eita.org.br)
*
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
interface ILDAPGroupPlugin {
/**
* Check if plugin implements actions
* @return int
*
* Returns the supported actions as int to be
* compared with OC_GROUP_BACKEND_CREATE_GROUP etc.
*/
public function respondToActions();
/**
* @param string $gid
* @return string|null The group DN if group creation was successful.
*/
public function createGroup($gid);
/**
* delete a group
* @param string $gid gid of the group to delete
* @return bool
*/
public function deleteGroup($gid);
/**
* Add a user to a group
* @param string $uid Name of the user to add to group
* @param string $gid Name of the group in which add the user
* @return bool
*
* Adds a user to a group.
*/
public function addToGroup($uid, $gid);
/**
* Removes a user from a group
* @param string $uid Name of the user to remove from group
* @param string $gid Name of the group from which remove the user
* @return bool
*
* removes the user from a group.
*/
public function removeFromGroup($uid, $gid);
/**
* get the number of all users matching the search string in a group
* @param string $gid
* @param string $search
* @return int|false
*/
public function countUsersInGroup($gid, $search = '');
/**
* get an array with group details
* @param string $gid
* @return array|false
*/
public function getGroupDetails($gid);
}
@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (c) 2017 EITA Cooperative (eita.org.br)
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
interface ILDAPUserPlugin {
/**
* Check if plugin implements actions
* @return int
*
* Returns the supported actions as int to be
* compared with OC_USER_BACKEND_CREATE_USER etc.
*/
public function respondToActions();
/**
* Create a new user in LDAP Backend
*
* @param string $uid The UID of the user to create
* @param string $password The password of the new user
* @return bool|string
*/
public function createUser($uid, $password);
/**
* Set password
*
* @param string $uid The username
* @param string $password The new password
* @return bool
*
* Change the password of a user
*/
public function setPassword($uid, $password);
/**
* get the user's home directory
* @param string $uid the username
* @return boolean
*/
public function getHome($uid);
/**
* get display name of the user
* @param string $uid user ID of the user
* @return string display name
*/
public function getDisplayName($uid);
/**
* set display name of the user
* @param string $uid user ID of the user
* @param string $displayName new user's display name
* @return string display name
*/
public function setDisplayName($uid, $displayName);
/**
* checks whether the user is allowed to change his avatar in Nextcloud
* @param string $uid the Nextcloud user name
* @return boolean either the user can or cannot
*/
public function canChangeAvatar($uid);
/**
* Count the number of users
* @return int|false
*/
public function countUsers();
}
@@ -0,0 +1,211 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roger Szabo <roger.szabo@web.de>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
interface ILDAPWrapper {
//LDAP functions in use
/**
* Bind to LDAP directory
* @param resource|\LDAP\Connection $link LDAP link resource
* @param string $dn an RDN to log in with
* @param string $password the password
* @return bool true on success, false otherwise
*
* with $dn and $password as null a anonymous bind is attempted.
*/
public function bind($link, $dn, $password);
/**
* connect to an LDAP server
* @param string $host The host to connect to
* @param string $port The port to connect to
* @return resource|\LDAP\Connection|false a link resource on success, otherwise false
*/
public function connect($host, $port);
/**
* Retrieve the LDAP pagination cookie
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\Result $result LDAP result resource
* @param string &$cookie structure sent by LDAP server
* @return bool true on success, false otherwise
*
* Corresponds to ldap_control_paged_result_response
*/
public function controlPagedResultResponse($link, $result, &$cookie);
/**
* Count the number of entries in a search
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\Result $result LDAP result resource
* @return int|false number of results on success, false otherwise
*/
public function countEntries($link, $result);
/**
* Return the LDAP error number of the last LDAP command
* @param resource|\LDAP\Connection $link LDAP link resource
* @return int error code
*/
public function errno($link);
/**
* Return the LDAP error message of the last LDAP command
* @param resource|\LDAP\Connection $link LDAP link resource
* @return string error message
*/
public function error($link);
/**
* Splits DN into its component parts
* @param string $dn
* @param int @withAttrib
* @return array|false
* @link https://www.php.net/manual/en/function.ldap-explode-dn.php
*/
public function explodeDN($dn, $withAttrib);
/**
* Return first result id
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\Result $result LDAP result resource
* @return resource|\LDAP\ResultEntry an LDAP entry resource
* */
public function firstEntry($link, $result);
/**
* Get attributes from a search result entry
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\ResultEntry $result LDAP result resource
* @return array|false containing the results, false on error
* */
public function getAttributes($link, $result);
/**
* Get the DN of a result entry
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\ResultEntry $result LDAP result resource
* @return string|false containing the DN, false on error
*/
public function getDN($link, $result);
/**
* Get all result entries
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\Result $result LDAP result resource
* @return array|false containing the results, false on error
*/
public function getEntries($link, $result);
/**
* Return next result id
* @param resource|\LDAP\Connection $link LDAP link resource
* @param resource|\LDAP\ResultEntry $result LDAP result resource
* @return resource|\LDAP\ResultEntry an LDAP entry resource
* */
public function nextEntry($link, $result);
/**
* Read an entry
* @param resource|\LDAP\Connection $link LDAP link resource
* @param string $baseDN The DN of the entry to read from
* @param string $filter An LDAP filter
* @param array $attr array of the attributes to read
* @return resource|\LDAP\Result an LDAP search result resource
*/
public function read($link, $baseDN, $filter, $attr);
/**
* Search LDAP tree
* @param resource|\LDAP\Connection $link LDAP link resource
* @param string $baseDN The DN of the entry to read from
* @param string $filter An LDAP filter
* @param array $attr array of the attributes to read
* @param int $attrsOnly optional, 1 if only attribute types shall be returned
* @param int $limit optional, limits the result entries
* @return resource|\LDAP\Result|false an LDAP search result resource, false on error
*/
public function search($link, string $baseDN, string $filter, array $attr, int $attrsOnly = 0, int $limit = 0, int $pageSize = 0, string $cookie = '');
/**
* Replace the value of a userPassword by $password
* @param resource|\LDAP\Connection $link LDAP link resource
* @param string $userDN the DN of the user whose password is to be replaced
* @param string $password the new value for the userPassword
* @return bool true on success, false otherwise
*/
public function modReplace($link, $userDN, $password);
/**
* Performs a PASSWD extended operation.
* @param resource|\LDAP\Connection $link LDAP link resource
* @return bool|string The generated password if new_password is empty or omitted. Otherwise true on success and false on failure.
*/
public function exopPasswd($link, string $userDN, string $oldPassword, string $password);
/**
* Sets the value of the specified option to be $value
* @param resource|\LDAP\Connection $link LDAP link resource
* @param int $option a defined LDAP Server option
* @param mixed $value the new value for the option
* @return bool true on success, false otherwise
*/
public function setOption($link, $option, $value);
/**
* establish Start TLS
* @param resource|\LDAP\Connection $link LDAP link resource
* @return bool true on success, false otherwise
*/
public function startTls($link);
/**
* Unbind from LDAP directory
* @param resource|\LDAP\Connection $link LDAP link resource
* @return bool true on success, false otherwise
*/
public function unbind($link);
//additional required methods in Nextcloud
/**
* Checks whether the server supports LDAP
* @return bool true if it the case, false otherwise
* */
public function areLDAPFunctionsAvailable();
/**
* Checks whether the submitted parameter is a resource
* @param mixed $resource the resource variable to check
* @return bool true if it is a resource or LDAP object, false otherwise
*/
public function isResource($resource);
}
@@ -0,0 +1,50 @@
<?php
/**
* @copyright Copyright (c) 2016, Roger Szabo (roger.szabo@web.de)
*
* @author Roger Szabo <roger.szabo@web.de>
* @author root <root@localhost.localdomain>
*
* @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\User_LDAP;
interface IUserLDAP {
//Functions used by LDAPProvider
/**
* Return access for LDAP interaction.
* @param string $uid
* @return Access instance of Access for LDAP interaction
*/
public function getLDAPAccess($uid);
/**
* Return a new LDAP connection for the specified user.
* @param string $uid
* @return resource|\LDAP\Connection of the LDAP connection
*/
public function getNewLDAPConnection($uid);
/**
* Return the username for the given LDAP DN, if available.
* @param string $dn
* @return string|false with the username
*/
public function dn2UserName($dn);
}
@@ -0,0 +1,220 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Jobs;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User_LDAP;
use OCA\User_LDAP\User_Proxy;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
/**
* Class CleanUp
*
* a Background job to clean up deleted users
*
* @package OCA\User_LDAP\Jobs;
*/
class CleanUp extends TimedJob {
/** @var ?int $limit amount of users that should be checked per run */
protected $limit;
/** @var int $defaultIntervalMin default interval in minutes */
protected $defaultIntervalMin = 51;
/** @var User_LDAP|User_Proxy $userBackend */
protected $userBackend;
/** @var \OCP\IConfig $ocConfig */
protected $ocConfig;
/** @var \OCP\IDBConnection $db */
protected $db;
/** @var Helper $ldapHelper */
protected $ldapHelper;
/** @var UserMapping */
protected $mapping;
/** @var DeletedUsersIndex */
protected $dui;
public function __construct(
ITimeFactory $timeFactory,
User_Proxy $userBackend,
DeletedUsersIndex $dui
) {
parent::__construct($timeFactory);
$minutes = \OC::$server->getConfig()->getSystemValue(
'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
$this->setInterval((int)$minutes * 60);
$this->userBackend = $userBackend;
$this->dui = $dui;
}
/**
* assigns the instances passed to run() to the class properties
* @param array $arguments
*/
public function setArguments($arguments): void {
//Dependency Injection is not possible, because the constructor will
//only get values that are serialized to JSON. I.e. whatever we would
//pass in app.php we do add here, except something else is passed e.g.
//in tests.
if (isset($arguments['helper'])) {
$this->ldapHelper = $arguments['helper'];
} else {
$this->ldapHelper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
}
if (isset($arguments['ocConfig'])) {
$this->ocConfig = $arguments['ocConfig'];
} else {
$this->ocConfig = \OC::$server->getConfig();
}
if (isset($arguments['userBackend'])) {
$this->userBackend = $arguments['userBackend'];
}
if (isset($arguments['db'])) {
$this->db = $arguments['db'];
} else {
$this->db = \OC::$server->getDatabaseConnection();
}
if (isset($arguments['mapping'])) {
$this->mapping = $arguments['mapping'];
} else {
$this->mapping = \OCP\Server::get(UserMapping::class);
}
if (isset($arguments['deletedUsersIndex'])) {
$this->dui = $arguments['deletedUsersIndex'];
}
}
/**
* makes the background job do its work
* @param array $argument
*/
public function run($argument): void {
$this->setArguments($argument);
if (!$this->isCleanUpAllowed()) {
return;
}
$users = $this->mapping->getList($this->getOffset(), $this->getChunkSize());
$resetOffset = $this->isOffsetResetNecessary(count($users));
$this->checkUsers($users);
$this->setOffset($resetOffset);
}
/**
* checks whether next run should start at 0 again
*/
public function isOffsetResetNecessary(int $resultCount): bool {
return $resultCount < $this->getChunkSize();
}
/**
* checks whether cleaning up LDAP users is allowed
*/
public function isCleanUpAllowed(): bool {
try {
if ($this->ldapHelper->haveDisabledConfigurations()) {
return false;
}
} catch (\Exception $e) {
return false;
}
return $this->isCleanUpEnabled();
}
/**
* checks whether clean up is enabled by configuration
*/
private function isCleanUpEnabled(): bool {
return (bool)$this->ocConfig->getSystemValue(
'ldapUserCleanupInterval', (string)$this->defaultIntervalMin);
}
/**
* checks users whether they are still existing
* @param array $users result from getMappedUsers()
*/
private function checkUsers(array $users): void {
foreach ($users as $user) {
$this->checkUser($user);
}
}
/**
* checks whether a user is still existing in LDAP
* @param string[] $user
*/
private function checkUser(array $user): void {
if ($this->userBackend->userExistsOnLDAP($user['name'])) {
//still available, all good
return;
}
$this->dui->markUser($user['name']);
}
/**
* gets the offset to fetch users from the mappings table
*/
private function getOffset(): int {
return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', '0');
}
/**
* sets the new offset for the next run
* @param bool $reset whether the offset should be set to 0
*/
public function setOffset(bool $reset = false): void {
$newOffset = $reset ? 0 :
$this->getOffset() + $this->getChunkSize();
$this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', (string)$newOffset);
}
/**
* returns the chunk size (limit in DB speak)
*/
public function getChunkSize(): int {
if ($this->limit === null) {
$this->limit = (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobChunkSize', '50');
}
return $this->limit;
}
}
@@ -0,0 +1,367 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @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\User_LDAP\Jobs;
use OC\ServerNotAvailableException;
use OCA\User_LDAP\AccessFactory;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\ConnectionFactory;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\LDAP;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\Notification\IManager;
use Psr\Log\LoggerInterface;
class Sync extends TimedJob {
public const MAX_INTERVAL = 12 * 60 * 60; // 12h
public const MIN_INTERVAL = 30 * 60; // 30min
/** @var Helper */
protected $ldapHelper;
/** @var LDAP */
protected $ldap;
/** @var UserMapping */
protected $mapper;
/** @var IConfig */
protected $config;
/** @var IAvatarManager */
protected $avatarManager;
/** @var IDBConnection */
protected $dbc;
/** @var IUserManager */
protected $ncUserManager;
/** @var LoggerInterface */
protected $logger;
/** @var IManager */
protected $notificationManager;
/** @var ConnectionFactory */
protected $connectionFactory;
/** @var AccessFactory */
protected $accessFactory;
public function __construct(ITimeFactory $time) {
parent::__construct($time);
$this->setInterval(
(int)\OC::$server->getConfig()->getAppValue(
'user_ldap',
'background_sync_interval',
(string)self::MIN_INTERVAL
)
);
}
/**
* updates the interval
*
* the idea is to adjust the interval depending on the amount of known users
* and the attempt to update each user one day. At most it would run every
* 30 minutes, and at least every 12 hours.
*/
public function updateInterval() {
$minPagingSize = $this->getMinPagingSize();
$mappedUsers = $this->mapper->count();
$runsPerDay = ($minPagingSize === 0 || $mappedUsers === 0) ? self::MAX_INTERVAL
: $mappedUsers / $minPagingSize;
$interval = floor(24 * 60 * 60 / $runsPerDay);
$interval = min(max($interval, self::MIN_INTERVAL), self::MAX_INTERVAL);
$this->config->setAppValue('user_ldap', 'background_sync_interval', (string)$interval);
}
/**
* returns the smallest configured paging size
* @return int
*/
protected function getMinPagingSize() {
$configKeys = $this->config->getAppKeys('user_ldap');
$configKeys = array_filter($configKeys, function ($key) {
return str_contains($key, 'ldap_paging_size');
});
$minPagingSize = null;
foreach ($configKeys as $configKey) {
$pagingSize = $this->config->getAppValue('user_ldap', $configKey, $minPagingSize);
$minPagingSize = $minPagingSize === null ? $pagingSize : min($minPagingSize, $pagingSize);
}
return (int)$minPagingSize;
}
/**
* @param array $argument
*/
public function run($argument) {
$this->setArgument($argument);
$isBackgroundJobModeAjax = $this->config
->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
if ($isBackgroundJobModeAjax) {
return;
}
$cycleData = $this->getCycle();
if ($cycleData === null) {
$cycleData = $this->determineNextCycle();
if ($cycleData === null) {
$this->updateInterval();
return;
}
}
if (!$this->qualifiesToRun($cycleData)) {
$this->updateInterval();
return;
}
try {
$expectMoreResults = $this->runCycle($cycleData);
if ($expectMoreResults) {
$this->increaseOffset($cycleData);
} else {
$this->determineNextCycle($cycleData);
}
$this->updateInterval();
} catch (ServerNotAvailableException $e) {
$this->determineNextCycle($cycleData);
}
}
/**
* @param array $cycleData
* @return bool whether more results are expected from the same configuration
*/
public function runCycle($cycleData) {
$connection = $this->connectionFactory->get($cycleData['prefix']);
$access = $this->accessFactory->get($connection);
$access->setUserMapper($this->mapper);
$filter = $access->combineFilterWithAnd([
$access->connection->ldapUserFilter,
$access->connection->ldapUserDisplayName . '=*',
$access->getFilterPartForUserSearch('')
]);
$results = $access->fetchListOfUsers(
$filter,
$access->userManager->getAttributes(),
$connection->ldapPagingSize,
$cycleData['offset'],
true
);
if ((int)$connection->ldapPagingSize === 0) {
return false;
}
return count($results) >= (int)$connection->ldapPagingSize;
}
/**
* returns the info about the current cycle that should be run, if any,
* otherwise null
*
* @return array|null
*/
public function getCycle() {
$prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
if (count($prefixes) === 0) {
return null;
}
$cycleData = [
'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', null),
'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', '0'),
];
if (
$cycleData['prefix'] !== null
&& in_array($cycleData['prefix'], $prefixes)
) {
return $cycleData;
}
return null;
}
/**
* Save the provided cycle information in the DB
*
* @param array $cycleData
*/
public function setCycle(array $cycleData) {
$this->config->setAppValue('user_ldap', 'background_sync_prefix', $cycleData['prefix']);
$this->config->setAppValue('user_ldap', 'background_sync_offset', $cycleData['offset']);
}
/**
* returns data about the next cycle that should run, if any, otherwise
* null. It also always goes for the next LDAP configuration!
*
* @param array|null $cycleData the old cycle
* @return array|null
*/
public function determineNextCycle(array $cycleData = null) {
$prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
if (count($prefixes) === 0) {
return null;
}
// get the next prefix in line and remember it
$oldPrefix = $cycleData === null ? null : $cycleData['prefix'];
$prefix = $this->getNextPrefix($oldPrefix);
if ($prefix === null) {
return null;
}
$cycleData['prefix'] = $prefix;
$cycleData['offset'] = 0;
$this->setCycle(['prefix' => $prefix, 'offset' => 0]);
return $cycleData;
}
/**
* Checks whether the provided cycle should be run. Currently only the
* last configuration change goes into account (at least one hour).
*
* @param $cycleData
* @return bool
*/
public function qualifiesToRun($cycleData) {
$lastChange = (int)$this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', '0');
if ((time() - $lastChange) > 60 * 30) {
return true;
}
return false;
}
/**
* increases the offset of the current cycle for the next run
*
* @param $cycleData
*/
protected function increaseOffset($cycleData) {
$ldapConfig = new Configuration($cycleData['prefix']);
$cycleData['offset'] += (int)$ldapConfig->ldapPagingSize;
$this->setCycle($cycleData);
}
/**
* determines the next configuration prefix based on the last one (if any)
*
* @param string|null $lastPrefix
* @return string|null
*/
protected function getNextPrefix($lastPrefix) {
$prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true);
$noOfPrefixes = count($prefixes);
if ($noOfPrefixes === 0) {
return null;
}
$i = $lastPrefix === null ? false : array_search($lastPrefix, $prefixes, true);
if ($i === false) {
$i = -1;
} else {
$i++;
}
if (!isset($prefixes[$i])) {
$i = 0;
}
return $prefixes[$i];
}
/**
* "fixes" DI
*/
public function setArgument($argument) {
if (isset($argument['config'])) {
$this->config = $argument['config'];
} else {
$this->config = \OC::$server->getConfig();
}
if (isset($argument['helper'])) {
$this->ldapHelper = $argument['helper'];
} else {
$this->ldapHelper = new Helper($this->config, \OC::$server->getDatabaseConnection());
}
if (isset($argument['ldapWrapper'])) {
$this->ldap = $argument['ldapWrapper'];
} else {
$this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file'));
}
if (isset($argument['avatarManager'])) {
$this->avatarManager = $argument['avatarManager'];
} else {
$this->avatarManager = \OC::$server->getAvatarManager();
}
if (isset($argument['dbc'])) {
$this->dbc = $argument['dbc'];
} else {
$this->dbc = \OC::$server->getDatabaseConnection();
}
if (isset($argument['ncUserManager'])) {
$this->ncUserManager = $argument['ncUserManager'];
} else {
$this->ncUserManager = \OC::$server->getUserManager();
}
if (isset($argument['logger'])) {
$this->logger = $argument['logger'];
} else {
$this->logger = \OC::$server->get(LoggerInterface::class);
}
if (isset($argument['notificationManager'])) {
$this->notificationManager = $argument['notificationManager'];
} else {
$this->notificationManager = \OC::$server->getNotificationManager();
}
if (isset($argument['mapper'])) {
$this->mapper = $argument['mapper'];
} else {
$this->mapper = \OCP\Server::get(UserMapping::class);
}
if (isset($argument['connectionFactory'])) {
$this->connectionFactory = $argument['connectionFactory'];
} else {
$this->connectionFactory = new ConnectionFactory($this->ldap);
}
if (isset($argument['accessFactory'])) {
$this->accessFactory = $argument['accessFactory'];
} else {
$this->accessFactory = \OCP\Server::get(AccessFactory::class);
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Jobs;
use OCA\User_LDAP\Service\UpdateGroupsService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\Exception;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
class UpdateGroups extends TimedJob {
public function __construct(
private UpdateGroupsService $service,
private LoggerInterface $logger,
IConfig $config,
ITimeFactory $timeFactory,
) {
parent::__construct($timeFactory);
$this->interval = (int)$config->getAppValue('user_ldap', 'bgjRefreshInterval', '3600');
}
/**
* @param mixed $argument
* @throws Exception
*/
public function run($argument): void {
$this->logger->debug('Run background job "updateGroups"');
$this->service->updateGroups();
}
}
+424
View File
@@ -0,0 +1,424 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Alexander Bergolth <leo@strike.wu.ac.at>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Peter Kubica <peter@kubica.ch>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OC\ServerNotAvailableException;
use OCA\User_LDAP\DataCollector\LdapDataCollector;
use OCA\User_LDAP\Exceptions\ConstraintViolationException;
use OCP\IConfig;
use OCP\Profiler\IProfiler;
use Psr\Log\LoggerInterface;
class LDAP implements ILDAPWrapper {
protected string $logFile = '';
protected array $curArgs = [];
protected LoggerInterface $logger;
private ?LdapDataCollector $dataCollector = null;
public function __construct(string $logFile = '') {
$this->logFile = $logFile;
/** @var IProfiler $profiler */
$profiler = \OC::$server->get(IProfiler::class);
if ($profiler->isEnabled()) {
$this->dataCollector = new LdapDataCollector();
$profiler->add($this->dataCollector);
}
$this->logger = \OCP\Server::get(LoggerInterface::class);
}
/**
* {@inheritDoc}
*/
public function bind($link, $dn, $password) {
return $this->invokeLDAPMethod('bind', $link, $dn, $password);
}
/**
* {@inheritDoc}
*/
public function connect($host, $port) {
$pos = strpos($host, '://');
if ($pos === false) {
$host = 'ldap://' . $host;
$pos = 4;
}
if (strpos($host, ':', $pos + 1) === false && !empty($port)) {
//ldap_connect ignores port parameter when URLs are passed
$host .= ':' . $port;
}
return $this->invokeLDAPMethod('connect', $host);
}
/**
* {@inheritDoc}
*/
public function controlPagedResultResponse($link, $result, &$cookie): bool {
$errorCode = 0;
$errorMsg = '';
$controls = [];
$matchedDn = null;
$referrals = [];
/** Cannot use invokeLDAPMethod because arguments are passed by reference */
$this->preFunctionCall('ldap_parse_result', [$link, $result]);
$success = ldap_parse_result($link, $result,
$errorCode,
$matchedDn,
$errorMsg,
$referrals,
$controls);
if ($errorCode !== 0) {
$this->processLDAPError($link, 'ldap_parse_result', $errorCode, $errorMsg);
}
if ($this->dataCollector !== null) {
$this->dataCollector->stopLastLdapRequest();
}
$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '';
return $success;
}
/**
* {@inheritDoc}
*/
public function countEntries($link, $result) {
return $this->invokeLDAPMethod('count_entries', $link, $result);
}
/**
* {@inheritDoc}
*/
public function errno($link) {
return $this->invokeLDAPMethod('errno', $link);
}
/**
* {@inheritDoc}
*/
public function error($link) {
return $this->invokeLDAPMethod('error', $link);
}
/**
* Splits DN into its component parts
* @param string $dn
* @param int $withAttrib
* @return array|false
* @link https://www.php.net/manual/en/function.ldap-explode-dn.php
*/
public function explodeDN($dn, $withAttrib) {
return $this->invokeLDAPMethod('explode_dn', $dn, $withAttrib);
}
/**
* {@inheritDoc}
*/
public function firstEntry($link, $result) {
return $this->invokeLDAPMethod('first_entry', $link, $result);
}
/**
* {@inheritDoc}
*/
public function getAttributes($link, $result) {
return $this->invokeLDAPMethod('get_attributes', $link, $result);
}
/**
* {@inheritDoc}
*/
public function getDN($link, $result) {
return $this->invokeLDAPMethod('get_dn', $link, $result);
}
/**
* {@inheritDoc}
*/
public function getEntries($link, $result) {
return $this->invokeLDAPMethod('get_entries', $link, $result);
}
/**
* {@inheritDoc}
*/
public function nextEntry($link, $result) {
return $this->invokeLDAPMethod('next_entry', $link, $result);
}
/**
* {@inheritDoc}
*/
public function read($link, $baseDN, $filter, $attr) {
return $this->invokeLDAPMethod('read', $link, $baseDN, $filter, $attr, 0, -1);
}
/**
* {@inheritDoc}
*/
public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = 0, int $pageSize = 0, string $cookie = '') {
if ($pageSize > 0 || $cookie !== '') {
$serverControls = [[
'oid' => LDAP_CONTROL_PAGEDRESULTS,
'value' => [
'size' => $pageSize,
'cookie' => $cookie,
],
'iscritical' => false,
]];
} else {
$serverControls = [];
}
$oldHandler = set_error_handler(function ($no, $message, $file, $line) use (&$oldHandler) {
if (str_contains($message, 'Partial search results returned: Sizelimit exceeded')) {
return true;
}
$oldHandler($no, $message, $file, $line);
return true;
});
try {
$result = $this->invokeLDAPMethod('search', $link, $baseDN, $filter, $attr, $attrsOnly, $limit, -1, LDAP_DEREF_NEVER, $serverControls);
restore_error_handler();
return $result;
} catch (\Exception $e) {
restore_error_handler();
throw $e;
}
}
/**
* {@inheritDoc}
*/
public function modReplace($link, $userDN, $password) {
return $this->invokeLDAPMethod('mod_replace', $link, $userDN, ['userPassword' => $password]);
}
/**
* {@inheritDoc}
*/
public function exopPasswd($link, string $userDN, string $oldPassword, string $password) {
return $this->invokeLDAPMethod('exop_passwd', $link, $userDN, $oldPassword, $password);
}
/**
* {@inheritDoc}
*/
public function setOption($link, $option, $value) {
return $this->invokeLDAPMethod('set_option', $link, $option, $value);
}
/**
* {@inheritDoc}
*/
public function startTls($link) {
return $this->invokeLDAPMethod('start_tls', $link);
}
/**
* {@inheritDoc}
*/
public function unbind($link) {
return $this->invokeLDAPMethod('unbind', $link);
}
/**
* Checks whether the server supports LDAP
* @return boolean if it the case, false otherwise
* */
public function areLDAPFunctionsAvailable() {
return function_exists('ldap_connect');
}
/**
* {@inheritDoc}
*/
public function isResource($resource) {
return is_resource($resource) || is_object($resource);
}
/**
* Checks whether the return value from LDAP is wrong or not.
*
* When using ldap_search we provide an array, in case multiple bases are
* configured. Thus, we need to check the array elements.
*
* @param mixed $result
*/
protected function isResultFalse(string $functionName, $result): bool {
if ($result === false) {
return true;
}
if ($functionName === 'ldap_search' && is_array($result)) {
foreach ($result as $singleResult) {
if ($singleResult === false) {
return true;
}
}
}
return false;
}
/**
* @param array $arguments
* @return mixed
*/
protected function invokeLDAPMethod(string $func, ...$arguments) {
$func = 'ldap_' . $func;
if (function_exists($func)) {
$this->preFunctionCall($func, $arguments);
$result = call_user_func_array($func, $arguments);
if ($this->isResultFalse($func, $result)) {
$this->postFunctionCall($func);
}
if ($this->dataCollector !== null) {
$this->dataCollector->stopLastLdapRequest();
}
return $result;
}
return null;
}
private function preFunctionCall(string $functionName, array $args): void {
$this->curArgs = $args;
if(strcasecmp($functionName, 'ldap_bind') === 0) {
// The arguments are not key value pairs
// \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw
// Remove it via direct array access for now, although a better solution could be found mebbe?
// @link https://github.com/nextcloud/server/issues/38461
$args[2] = IConfig::SENSITIVE_VALUE;
}
$this->logger->debug('Calling LDAP function {func} with parameters {args}', [
'app' => 'user_ldap',
'func' => $functionName,
'args' => json_encode($args),
]);
if ($this->dataCollector !== null) {
$args = array_map(function ($item) {
if ($this->isResource($item)) {
return '(resource)';
}
if (isset($item[0]['value']['cookie']) && $item[0]['value']['cookie'] !== "") {
$item[0]['value']['cookie'] = "*opaque cookie*";
}
return $item;
}, $this->curArgs);
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$this->dataCollector->startLdapRequest($functionName, $args, $backtrace);
}
if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs);
file_put_contents(
$this->logFile,
$functionName . '::' . json_encode($args) . "\n",
FILE_APPEND
);
}
}
/**
* Analyzes the returned LDAP error and acts accordingly if not 0
*
* @param resource|\LDAP\Connection $resource the LDAP Connection resource
* @throws ConstraintViolationException
* @throws ServerNotAvailableException
* @throws \Exception
*/
private function processLDAPError($resource, string $functionName, int $errorCode, string $errorMsg): void {
$this->logger->debug('LDAP error {message} ({code}) after calling {func}', [
'app' => 'user_ldap',
'message' => $errorMsg,
'code' => $errorCode,
'func' => $functionName,
]);
if ($functionName === 'ldap_get_entries'
&& $errorCode === -4) {
} elseif ($errorCode === 32) {
//for now
} elseif ($errorCode === 10) {
//referrals, we switch them off, but then there is AD :)
} elseif ($errorCode === -1) {
throw new ServerNotAvailableException('Lost connection to LDAP server.');
} elseif ($errorCode === 52) {
throw new ServerNotAvailableException('LDAP server is shutting down.');
} elseif ($errorCode === 48) {
throw new \Exception('LDAP authentication method rejected', $errorCode);
} elseif ($errorCode === 1) {
throw new \Exception('LDAP Operations error', $errorCode);
} elseif ($errorCode === 19) {
ldap_get_option($resource, LDAP_OPT_ERROR_STRING, $extended_error);
throw new ConstraintViolationException(!empty($extended_error) ? $extended_error : $errorMsg, $errorCode);
}
}
/**
* Called after an ldap method is run to act on LDAP error if necessary
* @throw \Exception
*/
private function postFunctionCall(string $functionName): void {
if ($this->isResource($this->curArgs[0])) {
$resource = $this->curArgs[0];
} elseif (
$functionName === 'ldap_search'
&& is_array($this->curArgs[0])
&& $this->isResource($this->curArgs[0][0])
) {
// we use always the same LDAP connection resource, is enough to
// take the first one.
$resource = $this->curArgs[0][0];
} else {
return;
}
$errorCode = ldap_errno($resource);
if ($errorCode === 0) {
return;
}
$errorMsg = ldap_error($resource);
$this->processLDAPError($resource, $functionName, $errorCode, $errorMsg);
$this->curArgs = [];
}
}
@@ -0,0 +1,350 @@
<?php
/**
* @copyright Copyright (c) 2016, Roger Szabo (roger.szabo@web.de)
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author root <root@localhost.localdomain>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCP\IServerContainer;
use OCP\LDAP\IDeletionFlagSupport;
use OCP\LDAP\ILDAPProvider;
/**
* LDAP provider for pulic access to the LDAP backend.
*/
class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport {
private $userBackend;
private $groupBackend;
private $logger;
private $helper;
private $deletedUsersIndex;
/**
* Create new LDAPProvider
* @param \OCP\IServerContainer $serverContainer
* @param Helper $helper
* @param DeletedUsersIndex $deletedUsersIndex
* @throws \Exception if user_ldap app was not enabled
*/
public function __construct(IServerContainer $serverContainer, Helper $helper, DeletedUsersIndex $deletedUsersIndex) {
$this->logger = $serverContainer->getLogger();
$this->helper = $helper;
$this->deletedUsersIndex = $deletedUsersIndex;
$userBackendFound = false;
$groupBackendFound = false;
foreach ($serverContainer->getUserManager()->getBackends() as $backend) {
$this->logger->debug('instance '.get_class($backend).' user backend.', ['app' => 'user_ldap']);
if ($backend instanceof IUserLDAP) {
$this->userBackend = $backend;
$userBackendFound = true;
break;
}
}
foreach ($serverContainer->getGroupManager()->getBackends() as $backend) {
$this->logger->debug('instance '.get_class($backend).' group backend.', ['app' => 'user_ldap']);
if ($backend instanceof IGroupLDAP) {
$this->groupBackend = $backend;
$groupBackendFound = true;
break;
}
}
if (!$userBackendFound or !$groupBackendFound) {
throw new \Exception('To use the LDAPProvider, user_ldap app must be enabled');
}
}
/**
* Translate an user id to LDAP DN
* @param string $uid user id
* @return string with the LDAP DN
* @throws \Exception if translation was unsuccessful
*/
public function getUserDN($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
$result = $this->userBackend->getLDAPAccess($uid)->username2dn($uid);
if (!$result) {
throw new \Exception('Translation to LDAP DN unsuccessful');
}
return $result;
}
/**
* Translate a group id to LDAP DN.
* @param string $gid group id
* @return string
* @throws \Exception
*/
public function getGroupDN($gid) {
if (!$this->groupBackend->groupExists($gid)) {
throw new \Exception('Group id not found in LDAP');
}
$result = $this->groupBackend->getLDAPAccess($gid)->groupname2dn($gid);
if (!$result) {
throw new \Exception('Translation to LDAP DN unsuccessful');
}
return $result;
}
/**
* Translate a LDAP DN to an internal user name. If there is no mapping between
* the DN and the user name, a new one will be created.
* @param string $dn LDAP DN
* @return string with the internal user name
* @throws \Exception if translation was unsuccessful
*/
public function getUserName($dn) {
$result = $this->userBackend->dn2UserName($dn);
if (!$result) {
throw new \Exception('Translation to internal user name unsuccessful');
}
return $result;
}
/**
* Convert a stored DN so it can be used as base parameter for LDAP queries.
* @param string $dn the DN in question
* @return string
*/
public function DNasBaseParameter($dn) {
return $this->helper->DNasBaseParameter($dn);
}
/**
* Sanitize a DN received from the LDAP server.
* @param array $dn the DN in question
* @return array the sanitized DN
*/
public function sanitizeDN($dn) {
return $this->helper->sanitizeDN($dn);
}
/**
* Return a new LDAP connection resource for the specified user.
* The connection must be closed manually.
* @param string $uid user id
* @return resource|\LDAP\Connection The LDAP connection
* @throws \Exception if user id was not found in LDAP
*/
public function getLDAPConnection($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
return $this->userBackend->getNewLDAPConnection($uid);
}
/**
* Return a new LDAP connection resource for the specified user.
* The connection must be closed manually.
* @param string $gid group id
* @return resource|\LDAP\Connection The LDAP connection
* @throws \Exception if group id was not found in LDAP
*/
public function getGroupLDAPConnection($gid) {
if (!$this->groupBackend->groupExists($gid)) {
throw new \Exception('Group id not found in LDAP');
}
return $this->groupBackend->getNewLDAPConnection($gid);
}
/**
* Get the LDAP base for users.
* @param string $uid user id
* @return string the base for users
* @throws \Exception if user id was not found in LDAP
*/
public function getLDAPBaseUsers($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
$access = $this->userBackend->getLDAPAccess($uid);
$bases = $access->getConnection()->ldapBaseUsers;
$dn = $this->getUserDN($uid);
foreach ($bases as $base) {
if ($access->isDNPartOfBase($dn, [$base])) {
return $base;
}
}
// should not occur, because the user does not qualify to use NC in this case
$this->logger->info(
'No matching user base found for user {dn}, available: {bases}.',
[
'app' => 'user_ldap',
'dn' => $dn,
'bases' => $bases,
]
);
return array_shift($bases);
}
/**
* Get the LDAP base for groups.
* @param string $uid user id
* @return string the base for groups
* @throws \Exception if user id was not found in LDAP
*/
public function getLDAPBaseGroups($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
$bases = $this->userBackend->getLDAPAccess($uid)->getConnection()->ldapBaseGroups;
return array_shift($bases);
}
/**
* Clear the cache if a cache is used, otherwise do nothing.
* @param string $uid user id
* @throws \Exception if user id was not found in LDAP
*/
public function clearCache($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
$this->userBackend->getLDAPAccess($uid)->getConnection()->clearCache();
}
/**
* Clear the cache if a cache is used, otherwise do nothing.
* Acts on the LDAP connection of a group
* @param string $gid group id
* @throws \Exception if user id was not found in LDAP
*/
public function clearGroupCache($gid) {
if (!$this->groupBackend->groupExists($gid)) {
throw new \Exception('Group id not found in LDAP');
}
$this->groupBackend->getLDAPAccess($gid)->getConnection()->clearCache();
}
/**
* Check whether a LDAP DN exists
* @param string $dn LDAP DN
* @return bool whether the DN exists
*/
public function dnExists($dn) {
$result = $this->userBackend->dn2UserName($dn);
return !$result ? false : true;
}
/**
* Flag record for deletion.
* @param string $uid user id
*/
public function flagRecord($uid) {
$this->deletedUsersIndex->markUser($uid);
}
/**
* Unflag record for deletion.
* @param string $uid user id
*/
public function unflagRecord($uid) {
//do nothing
}
/**
* Get the LDAP attribute name for the user's display name
* @param string $uid user id
* @return string the display name field
* @throws \Exception if user id was not found in LDAP
*/
public function getLDAPDisplayNameField($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
return $this->userBackend->getLDAPAccess($uid)->getConnection()->getConfiguration()['ldap_display_name'];
}
/**
* Get the LDAP attribute name for the email
* @param string $uid user id
* @return string the email field
* @throws \Exception if user id was not found in LDAP
*/
public function getLDAPEmailField($uid) {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
return $this->userBackend->getLDAPAccess($uid)->getConnection()->getConfiguration()['ldap_email_attr'];
}
/**
* Get the LDAP type of association between users and groups
* @param string $gid group id
* @return string the configuration, one of: 'memberUid', 'uniqueMember', 'member', 'gidNumber', ''
* @throws \Exception if group id was not found in LDAP
*/
public function getLDAPGroupMemberAssoc($gid) {
if (!$this->groupBackend->groupExists($gid)) {
throw new \Exception('Group id not found in LDAP');
}
return $this->groupBackend->getLDAPAccess($gid)->getConnection()->getConfiguration()['ldap_group_member_assoc_attribute'];
}
/**
* Get an LDAP attribute for a nextcloud user
*
* @throws \Exception if user id was not found in LDAP
*/
public function getUserAttribute(string $uid, string $attribute): ?string {
$values = $this->getMultiValueUserAttribute($uid, $attribute);
if (count($values) === 0) {
return null;
}
return current($values);
}
/**
* Get a multi-value LDAP attribute for a nextcloud user
*
* @throws \Exception if user id was not found in LDAP
*/
public function getMultiValueUserAttribute(string $uid, string $attribute): array {
if (!$this->userBackend->userExists($uid)) {
throw new \Exception('User id not found in LDAP');
}
$access = $this->userBackend->getLDAPAccess($uid);
$connection = $access->getConnection();
$key = $uid . '-' . $attribute;
$cached = $connection->getFromCache($key);
if (is_array($cached)) {
return $cached;
}
$values = $access->readAttribute($access->username2dn($uid), $attribute);
if ($values === false) {
$values = [];
}
$connection->writeToCache($key, $values);
return $values;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2016, Roger Szabo (roger.szabo@web.de)
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author root <root@localhost.localdomain>
*
* @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\User_LDAP;
use OCP\IServerContainer;
use OCP\LDAP\ILDAPProvider;
use OCP\LDAP\ILDAPProviderFactory;
class LDAPProviderFactory implements ILDAPProviderFactory {
/** * @var IServerContainer */
private $serverContainer;
public function __construct(IServerContainer $serverContainer) {
$this->serverContainer = $serverContainer;
}
public function getLDAPProvider(): ILDAPProvider {
return $this->serverContainer->get(LDAPProvider::class);
}
public function isAvailable(): bool {
return true;
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
abstract class LDAPUtility {
protected ILDAPWrapper $ldap;
/**
* constructor, make sure the subclasses call this one!
* @param ILDAPWrapper $ldapWrapper an instance of an ILDAPWrapper
*/
public function __construct(ILDAPWrapper $ldapWrapper) {
$this->ldap = $ldapWrapper;
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Côme Chilliet <come.chilliet@nextcloud.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP;
use OCA\User_LDAP\Db\GroupMembership;
use OCA\User_LDAP\Db\GroupMembershipMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\User\Events\PostLoginEvent;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<PostLoginEvent>
*/
class LoginListener implements IEventListener {
public function __construct(
private IEventDispatcher $dispatcher,
private Group_Proxy $groupBackend,
private IGroupManager $groupManager,
private LoggerInterface $logger,
private GroupMembershipMapper $groupMembershipMapper,
) {
}
public function handle(Event $event): void {
if ($event instanceof PostLoginEvent) {
$this->onPostLogin($event->getUser());
}
}
public function onPostLogin(IUser $user): void {
$this->logger->info(
__CLASS__ . ' – {user} postLogin',
[
'app' => 'user_ldap',
'user' => $user->getUID(),
]
);
$this->updateGroups($user);
}
private function updateGroups(IUser $userObject): void {
$userId = $userObject->getUID();
$groupMemberships = $this->groupMembershipMapper->findGroupMembershipsForUser($userId);
$knownGroups = array_map(
static fn (GroupMembership $groupMembership): string => $groupMembership->getGroupid(),
$groupMemberships
);
$groupMemberships = array_combine($knownGroups, $groupMemberships);
$actualGroups = $this->groupBackend->getUserGroups($userId);
$newGroups = array_diff($actualGroups, $knownGroups);
$oldGroups = array_diff($knownGroups, $actualGroups);
foreach ($newGroups as $groupId) {
$groupObject = $this->groupManager->get($groupId);
if ($groupObject === null) {
$this->logger->error(
__CLASS__ . ' – group {group} could not be found (user {user})',
[
'app' => 'user_ldap',
'user' => $userId,
'group' => $groupId
]
);
continue;
}
$this->groupMembershipMapper->insert(GroupMembership::fromParams(['groupid' => $groupId,'userid' => $userId]));
$this->groupBackend->addRelationshipToCaches($userId, null, $groupId);
$this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject));
$this->logger->info(
__CLASS__ . ' – {user} added to {group}',
[
'app' => 'user_ldap',
'user' => $userId,
'group' => $groupId
]
);
}
foreach ($oldGroups as $groupId) {
$this->groupMembershipMapper->delete($groupMemberships[$groupId]);
$groupObject = $this->groupManager->get($groupId);
if ($groupObject === null) {
$this->logger->error(
__CLASS__ . ' – group {group} could not be found (user {user})',
[
'app' => 'user_ldap',
'user' => $userId,
'group' => $groupId
]
);
continue;
}
$this->dispatcher->dispatchTyped(new UserRemovedEvent($groupObject, $userObject));
$this->logger->info(
'service "updateGroups" – {user} removed from {group}',
[
'user' => $userId,
'group' => $groupId
]
);
}
}
}
@@ -0,0 +1,471 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Aaron Wood <aaronjwood@gmail.com>
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author blizzz <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Mapping;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use OCP\DB\IPreparedStatement;
use OCP\DB\QueryBuilder\IQueryBuilder;
/**
* Class AbstractMapping
*
* @package OCA\User_LDAP\Mapping
*/
abstract class AbstractMapping {
/**
* @var \OCP\IDBConnection $dbc
*/
protected $dbc;
/**
* returns the DB table name which holds the mappings
*
* @return string
*/
abstract protected function getTableName(bool $includePrefix = true);
/**
* @param \OCP\IDBConnection $dbc
*/
public function __construct(\OCP\IDBConnection $dbc) {
$this->dbc = $dbc;
}
/** @var array caches Names (value) by DN (key) */
protected $cache = [];
/**
* checks whether a provided string represents an existing table col
*
* @param string $col
* @return bool
*/
public function isColNameValid($col) {
switch ($col) {
case 'ldap_dn':
case 'ldap_dn_hash':
case 'owncloud_name':
case 'directory_uuid':
return true;
default:
return false;
}
}
/**
* Gets the value of one column based on a provided value of another column
*
* @param string $fetchCol
* @param string $compareCol
* @param string $search
* @return string|false
* @throws \Exception
*/
protected function getXbyY($fetchCol, $compareCol, $search) {
if (!$this->isColNameValid($fetchCol)) {
//this is used internally only, but we don't want to risk
//having SQL injection at all.
throw new \Exception('Invalid Column Name');
}
$query = $this->dbc->prepare('
SELECT `' . $fetchCol . '`
FROM `' . $this->getTableName() . '`
WHERE `' . $compareCol . '` = ?
');
try {
$res = $query->execute([$search]);
$data = $res->fetchOne();
$res->closeCursor();
return $data;
} catch (Exception $e) {
return false;
}
}
/**
* Performs a DELETE or UPDATE query to the database.
*
* @param IPreparedStatement $statement
* @param array $parameters
* @return bool true if at least one row was modified, false otherwise
*/
protected function modify(IPreparedStatement $statement, $parameters) {
try {
$result = $statement->execute($parameters);
$updated = $result->rowCount() > 0;
$result->closeCursor();
return $updated;
} catch (Exception $e) {
return false;
}
}
/**
* Gets the LDAP DN based on the provided name.
* Replaces Access::ocname2dn
*
* @param string $name
* @return string|false
*/
public function getDNByName($name) {
$dn = array_search($name, $this->cache);
if ($dn === false && ($dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name)) !== false) {
$this->cache[$dn] = $name;
}
return $dn;
}
/**
* Updates the DN based on the given UUID
*
* @param string $fdn
* @param string $uuid
* @return bool
*/
public function setDNbyUUID($fdn, $uuid) {
$oldDn = $this->getDnByUUID($uuid);
$statement = $this->dbc->prepare('
UPDATE `' . $this->getTableName() . '`
SET `ldap_dn_hash` = ?, `ldap_dn` = ?
WHERE `directory_uuid` = ?
');
$r = $this->modify($statement, [$this->getDNHash($fdn), $fdn, $uuid]);
if ($r && is_string($oldDn) && isset($this->cache[$oldDn])) {
$this->cache[$fdn] = $this->cache[$oldDn];
unset($this->cache[$oldDn]);
}
return $r;
}
/**
* Updates the UUID based on the given DN
*
* required by Migration/UUIDFix
*
* @param $uuid
* @param $fdn
* @return bool
*/
public function setUUIDbyDN($uuid, $fdn): bool {
$statement = $this->dbc->prepare('
UPDATE `' . $this->getTableName() . '`
SET `directory_uuid` = ?
WHERE `ldap_dn_hash` = ?
');
unset($this->cache[$fdn]);
return $this->modify($statement, [$uuid, $this->getDNHash($fdn)]);
}
/**
* Get the hash to store in database column ldap_dn_hash for a given dn
*/
protected function getDNHash(string $fdn): string {
return hash('sha256', $fdn, false);
}
/**
* Gets the name based on the provided LDAP DN.
*
* @param string $fdn
* @return string|false
*/
public function getNameByDN($fdn) {
if (!isset($this->cache[$fdn])) {
$this->cache[$fdn] = $this->getXbyY('owncloud_name', 'ldap_dn_hash', $this->getDNHash($fdn));
}
return $this->cache[$fdn];
}
/**
* @param array<string> $hashList
*/
protected function prepareListOfIdsQuery(array $hashList): IQueryBuilder {
$qb = $this->dbc->getQueryBuilder();
$qb->select('owncloud_name', 'ldap_dn_hash', 'ldap_dn')
->from($this->getTableName(false))
->where($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($hashList, IQueryBuilder::PARAM_STR_ARRAY)));
return $qb;
}
protected function collectResultsFromListOfIdsQuery(IQueryBuilder $qb, array &$results): void {
$stmt = $qb->executeQuery();
while ($entry = $stmt->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE)) {
$results[$entry['ldap_dn']] = $entry['owncloud_name'];
$this->cache[$entry['ldap_dn']] = $entry['owncloud_name'];
}
$stmt->closeCursor();
}
/**
* @param array<string> $fdns
* @return array<string,string>
*/
public function getListOfIdsByDn(array $fdns): array {
$totalDBParamLimit = 65000;
$sliceSize = 1000;
$maxSlices = $this->dbc->getDatabasePlatform() instanceof SqlitePlatform ? 9 : $totalDBParamLimit / $sliceSize;
$results = [];
$slice = 1;
$fdns = array_map([$this, 'getDNHash'], $fdns);
$fdnsSlice = count($fdns) > $sliceSize ? array_slice($fdns, 0, $sliceSize) : $fdns;
$qb = $this->prepareListOfIdsQuery($fdnsSlice);
while (isset($fdnsSlice[999])) {
// Oracle does not allow more than 1000 values in the IN list,
// but allows slicing
$slice++;
$fdnsSlice = array_slice($fdns, $sliceSize * ($slice - 1), $sliceSize);
/** @see https://github.com/vimeo/psalm/issues/4995 */
/** @psalm-suppress TypeDoesNotContainType */
if (!isset($qb)) {
$qb = $this->prepareListOfIdsQuery($fdnsSlice);
continue;
}
if (!empty($fdnsSlice)) {
$qb->orWhere($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($fdnsSlice, IQueryBuilder::PARAM_STR_ARRAY)));
}
if ($slice % $maxSlices === 0) {
$this->collectResultsFromListOfIdsQuery($qb, $results);
unset($qb);
}
}
if (isset($qb)) {
$this->collectResultsFromListOfIdsQuery($qb, $results);
}
return $results;
}
/**
* Searches mapped names by the giving string in the name column
*
* @return string[]
*/
public function getNamesBySearch(string $search, string $prefixMatch = "", string $postfixMatch = ""): array {
$statement = $this->dbc->prepare('
SELECT `owncloud_name`
FROM `' . $this->getTableName() . '`
WHERE `owncloud_name` LIKE ?
');
try {
$res = $statement->execute([$prefixMatch . $this->dbc->escapeLikeParameter($search) . $postfixMatch]);
} catch (Exception $e) {
return [];
}
$names = [];
while ($row = $res->fetch()) {
$names[] = $row['owncloud_name'];
}
return $names;
}
/**
* Gets the name based on the provided LDAP UUID.
*
* @param string $uuid
* @return string|false
*/
public function getNameByUUID($uuid) {
return $this->getXbyY('owncloud_name', 'directory_uuid', $uuid);
}
public function getDnByUUID($uuid) {
return $this->getXbyY('ldap_dn', 'directory_uuid', $uuid);
}
/**
* Gets the UUID based on the provided LDAP DN
*
* @param string $dn
* @return false|string
* @throws \Exception
*/
public function getUUIDByDN($dn) {
return $this->getXbyY('directory_uuid', 'ldap_dn_hash', $this->getDNHash($dn));
}
public function getList(int $offset = 0, int $limit = null, bool $invalidatedOnly = false): array {
$select = $this->dbc->getQueryBuilder();
$select->selectAlias('ldap_dn', 'dn')
->selectAlias('owncloud_name', 'name')
->selectAlias('directory_uuid', 'uuid')
->from($this->getTableName())
->setMaxResults($limit)
->setFirstResult($offset);
if ($invalidatedOnly) {
$select->where($select->expr()->like('directory_uuid', $select->createNamedParameter('invalidated_%')));
}
$result = $select->executeQuery();
$entries = $result->fetchAll();
$result->closeCursor();
return $entries;
}
/**
* attempts to map the given entry
*
* @param string $fdn fully distinguished name (from LDAP)
* @param string $name
* @param string $uuid a unique identifier as used in LDAP
* @return bool
*/
public function map($fdn, $name, $uuid) {
if (mb_strlen($fdn) > 4000) {
\OC::$server->getLogger()->error(
'Cannot map, because the DN exceeds 4000 characters: {dn}',
[
'app' => 'user_ldap',
'dn' => $fdn,
]
);
return false;
}
$row = [
'ldap_dn_hash' => $this->getDNHash($fdn),
'ldap_dn' => $fdn,
'owncloud_name' => $name,
'directory_uuid' => $uuid
];
try {
$result = $this->dbc->insertIfNotExist($this->getTableName(), $row);
if ((bool)$result === true) {
$this->cache[$fdn] = $name;
}
// insertIfNotExist returns values as int
return (bool)$result;
} catch (\Exception $e) {
return false;
}
}
/**
* removes a mapping based on the owncloud_name of the entry
*
* @param string $name
* @return bool
*/
public function unmap($name) {
$statement = $this->dbc->prepare('
DELETE FROM `' . $this->getTableName() . '`
WHERE `owncloud_name` = ?');
$dn = array_search($name, $this->cache);
if ($dn !== false) {
unset($this->cache[$dn]);
}
return $this->modify($statement, [$name]);
}
/**
* Truncates the mapping table
*
* @return bool
*/
public function clear() {
$sql = $this->dbc
->getDatabasePlatform()
->getTruncateTableSQL('`' . $this->getTableName() . '`');
try {
$this->dbc->executeQuery($sql);
return true;
} catch (Exception $e) {
return false;
}
}
/**
* clears the mapping table one by one and executing a callback with
* each row's id (=owncloud_name col)
*
* @param callable $preCallback
* @param callable $postCallback
* @return bool true on success, false when at least one row was not
* deleted
*/
public function clearCb(callable $preCallback, callable $postCallback): bool {
$picker = $this->dbc->getQueryBuilder();
$picker->select('owncloud_name')
->from($this->getTableName());
$cursor = $picker->executeQuery();
$result = true;
while (($id = $cursor->fetchOne()) !== false) {
$preCallback($id);
if ($isUnmapped = $this->unmap($id)) {
$postCallback($id);
}
$result = $result && $isUnmapped;
}
$cursor->closeCursor();
return $result;
}
/**
* returns the number of entries in the mappings table
*
* @return int
*/
public function count(): int {
$query = $this->dbc->getQueryBuilder();
$query->select($query->func()->count('ldap_dn_hash'))
->from($this->getTableName());
$res = $query->execute();
$count = $res->fetchOne();
$res->closeCursor();
return (int)$count;
}
public function countInvalidated(): int {
$query = $this->dbc->getQueryBuilder();
$query->select($query->func()->count('ldap_dn_hash'))
->from($this->getTableName())
->where($query->expr()->like('directory_uuid', $query->createNamedParameter('invalidated_%')));
$res = $query->execute();
$count = $res->fetchOne();
$res->closeCursor();
return (int)$count;
}
}
@@ -0,0 +1,39 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Mapping;
/**
* Class UserMapping
* @package OCA\User_LDAP\Mapping
*/
class GroupMapping extends AbstractMapping {
/**
* returns the DB table name which holds the mappings
* @return string
*/
protected function getTableName(bool $includePrefix = true) {
$p = $includePrefix ? '*PREFIX*' : '';
return $p . 'ldap_group_mapping';
}
}
@@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Mapping;
use OCP\HintException;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\Server;
use OCP\Support\Subscription\IAssertion;
/**
* Class UserMapping
*
* @package OCA\User_LDAP\Mapping
*/
class UserMapping extends AbstractMapping {
private IAssertion $assertion;
protected const PROV_API_REGEX = '/\/ocs\/v[1-9].php\/cloud\/(groups|users)/';
public function __construct(IDBConnection $dbc, IAssertion $assertion) {
$this->assertion = $assertion;
parent::__construct($dbc);
}
/**
* @throws HintException
*/
public function map($fdn, $name, $uuid): bool {
try {
$this->assertion->createUserIsLegit();
} catch (HintException $e) {
static $isProvisioningApi = null;
if ($isProvisioningApi === null) {
$request = Server::get(IRequest::class);
$isProvisioningApi = \preg_match(self::PROV_API_REGEX, $request->getRequestUri()) === 1;
}
if ($isProvisioningApi) {
// only throw when prov API is being used, since functionality
// should not break for end users (e.g. when sharing).
// On direct API usage, e.g. on users page, this is desired.
throw $e;
}
return false;
}
return parent::map($fdn, $name, $uuid);
}
/**
* returns the DB table name which holds the mappings
* @return string
*/
protected function getTableName(bool $includePrefix = true) {
$p = $includePrefix ? '*PREFIX*' : '';
return $p . 'ldap_user_mapping';
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Migration;
use OCP\IDBConnection;
use OCP\Migration\SimpleMigrationStep;
abstract class GroupMappingMigration extends SimpleMigrationStep {
/** @var IDBConnection */
private $dbc;
public function __construct(IDBConnection $dbc) {
$this->dbc = $dbc;
}
protected function copyGroupMappingData(string $sourceTable, string $destinationTable): void {
$insert = $this->dbc->getQueryBuilder();
$insert->insert($destinationTable)
->values([
'ldap_dn' => $insert->createParameter('ldap_dn'),
'owncloud_name' => $insert->createParameter('owncloud_name'),
'directory_uuid' => $insert->createParameter('directory_uuid'),
'ldap_dn_hash' => $insert->createParameter('ldap_dn_hash'),
]);
$query = $this->dbc->getQueryBuilder();
$query->select('*')
->from($sourceTable);
$result = $query->executeQuery();
while ($row = $result->fetch()) {
$insert
->setParameter('ldap_dn', $row['ldap_dn'])
->setParameter('owncloud_name', $row['owncloud_name'])
->setParameter('directory_uuid', $row['directory_uuid'])
->setParameter('ldap_dn_hash', $row['ldap_dn_hash'])
;
$insert->executeStatement();
}
$result->closeCursor();
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
/**
* Class RmRefreshTime
*
* this can be removed with Nextcloud 21
*
* @package OCA\User_LDAP\Migration
*/
class RemoveRefreshTime implements IRepairStep {
/** @var IDBConnection */
private $dbc;
/** @var IConfig */
private $config;
public function __construct(IDBConnection $dbc, IConfig $config) {
$this->dbc = $dbc;
$this->config = $config;
}
public function getName() {
return 'Remove deprecated refresh time markers for LDAP user records';
}
public function run(IOutput $output) {
$this->config->deleteAppValue('user_ldap', 'updateAttributesInterval');
$qb = $this->dbc->getQueryBuilder();
$qb->delete('preferences')
->where($qb->expr()->eq('appid', $qb->createNamedParameter('user_ldap')))
->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lastFeatureRefresh')))
->execute();
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 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\User_LDAP\Migration;
use OCA\User_LDAP\Helper;
use OCA\User_LDAP\LDAPProviderFactory;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class SetDefaultProvider implements IRepairStep {
/** @var IConfig */
private $config;
/** @var Helper */
private $helper;
public function __construct(IConfig $config,
Helper $helper) {
$this->config = $config;
$this->helper = $helper;
}
public function getName(): string {
return 'Set default LDAP provider';
}
public function run(IOutput $output): void {
$current = $this->config->getSystemValue('ldapProviderFactory', null);
if ($current === null) {
$this->config->setSystemValue('ldapProviderFactory', LDAPProviderFactory::class);
}
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author 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\User_LDAP\Migration;
use OC\BackgroundJob\QueuedJob;
use OCA\User_LDAP\Mapping\AbstractMapping;
use OCA\User_LDAP\Proxy;
use OCA\User_LDAP\User_Proxy;
abstract class UUIDFix extends QueuedJob {
/** @var AbstractMapping */
protected $mapper;
/** @var Proxy */
protected $proxy;
public function run($argument) {
$isUser = $this->proxy instanceof User_Proxy;
foreach ($argument['records'] as $record) {
$access = $this->proxy->getLDAPAccess($record['name']);
$uuid = $access->getUUID($record['dn'], $isUser);
if ($uuid === false) {
// record not found, no prob, continue with the next
continue;
}
if ($uuid !== $record['uuid']) {
$this->mapper->setUUIDbyDN($uuid, $record['dn']);
}
}
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use OCA\User_LDAP\Group_Proxy;
use OCA\User_LDAP\Mapping\GroupMapping;
class UUIDFixGroup extends UUIDFix {
public function __construct(GroupMapping $mapper, Group_Proxy $proxy) {
$this->mapper = $mapper;
$this->proxy = $proxy;
}
}
@@ -0,0 +1,101 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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\User_LDAP\Migration;
use OCA\User_LDAP\Mapping\GroupMapping;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\BackgroundJob\IJobList;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class UUIDFixInsert implements IRepairStep {
/** @var IConfig */
protected $config;
/** @var UserMapping */
protected $userMapper;
/** @var GroupMapping */
protected $groupMapper;
/** @var IJobList */
protected $jobList;
public function __construct(IConfig $config, UserMapping $userMapper, GroupMapping $groupMapper, IJobList $jobList) {
$this->config = $config;
$this->userMapper = $userMapper;
$this->groupMapper = $groupMapper;
$this->jobList = $jobList;
}
/**
* Returns the step's name
*
* @return string
* @since 9.1.0
*/
public function getName() {
return 'Insert UUIDFix background job for user and group in batches';
}
/**
* Run repair step.
* Must throw exception on error.
*
* @param IOutput $output
* @throws \Exception in case of failure
* @since 9.1.0
*/
public function run(IOutput $output) {
$installedVersion = $this->config->getAppValue('user_ldap', 'installed_version', '1.2.1');
if (version_compare($installedVersion, '1.2.1') !== -1) {
return;
}
foreach ([$this->userMapper, $this->groupMapper] as $mapper) {
$offset = 0;
$batchSize = 50;
$jobClass = $mapper instanceof UserMapping ? UUIDFixUser::class : UUIDFixGroup::class;
do {
$retry = false;
$records = $mapper->getList($offset, $batchSize);
if (count($records) === 0) {
continue;
}
try {
$this->jobList->add($jobClass, ['records' => $records]);
$offset += $batchSize;
} catch (\InvalidArgumentException $e) {
if (str_contains($e->getMessage(), 'Background job arguments can\'t exceed 4000')) {
$batchSize = (int)floor(count($records) * 0.8);
$retry = true;
}
}
} while (count($records) === $batchSize || $retry);
}
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\User_Proxy;
class UUIDFixUser extends UUIDFix {
public function __construct(UserMapping $mapper, User_Proxy $proxy) {
$this->mapper = $mapper;
$this->proxy = $proxy;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use OCA\User_LDAP\LDAPProviderFactory;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class UnsetDefaultProvider implements IRepairStep {
/** @var IConfig */
private $config;
public function __construct(IConfig $config) {
$this->config = $config;
}
public function getName(): string {
return 'Unset default LDAP provider';
}
public function run(IOutput $output): void {
$current = $this->config->getSystemValue('ldapProviderFactory', null);
if ($current === LDAPProviderFactory::class) {
$this->config->deleteSystemValue('ldapProviderFactory');
}
}
}
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1010Date20200630192842 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_user_mapping')) {
$table = $schema->createTable('ldap_user_mapping');
$table->addColumn('ldap_dn', Types::STRING, [
'notnull' => true,
'length' => 4000,
'default' => '',
]);
$table->addColumn('owncloud_name', Types::STRING, [
'notnull' => true,
'length' => 64,
'default' => '',
]);
$table->addColumn('directory_uuid', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('ldap_dn_hash', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->setPrimaryKey(['owncloud_name']);
$table->addUniqueIndex(['ldap_dn_hash'], 'ldap_user_dn_hashes');
$table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid');
}
if (!$schema->hasTable('ldap_group_mapping')) {
$table = $schema->createTable('ldap_group_mapping');
$table->addColumn('ldap_dn', Types::STRING, [
'notnull' => true,
'length' => 4000,
'default' => '',
]);
$table->addColumn('owncloud_name', Types::STRING, [
'notnull' => true,
'length' => 64,
'default' => '',
]);
$table->addColumn('directory_uuid', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('ldap_dn_hash', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->setPrimaryKey(['owncloud_name']);
$table->addUniqueIndex(['ldap_dn_hash'], 'ldap_group_dn_hashes');
$table->addUniqueIndex(['directory_uuid'], 'ldap_group_directory_uuid');
}
if (!$schema->hasTable('ldap_group_members')) {
$table = $schema->createTable('ldap_group_members');
$table->addColumn('owncloudname', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('owncloudusers', Types::TEXT, [
'notnull' => true,
]);
$table->setPrimaryKey(['owncloudname']);
}
return $schema;
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use Closure;
use OC\Hooks\PublicEmitter;
use OCP\DB\Exception;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Psr\Log\LoggerInterface;
class Version1120Date20210917155206 extends SimpleMigrationStep {
/** @var IDBConnection */
private $dbc;
/** @var IUserManager */
private $userManager;
/** @var LoggerInterface */
private $logger;
public function __construct(IDBConnection $dbc, IUserManager $userManager, LoggerInterface $logger) {
$this->dbc = $dbc;
$this->userManager = $userManager;
$this->logger = $logger;
}
public function getName() {
return 'Adjust LDAP user and group id column lengths to match server lengths';
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
// ensure that there is no user or group id longer than 64char in LDAP table
$this->handleIDs('ldap_group_mapping', false);
$this->handleIDs('ldap_user_mapping', true);
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changeSchema = false;
foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
$table = $schema->getTable($tableName);
$column = $table->getColumn('owncloud_name');
if ($column->getLength() > 64) {
$column->setLength(64);
$changeSchema = true;
}
}
return $changeSchema ? $schema : null;
}
protected function handleIDs(string $table, bool $emitHooks) {
$select = $this->getSelectQuery($table);
$update = $this->getUpdateQuery($table);
$result = $select->executeQuery();
while ($row = $result->fetch()) {
$newId = hash('sha256', $row['owncloud_name'], false);
if ($emitHooks) {
$this->emitUnassign($row['owncloud_name'], true);
}
$update->setParameter('uuid', $row['directory_uuid']);
$update->setParameter('newId', $newId);
try {
$update->executeStatement();
if ($emitHooks) {
$this->emitUnassign($row['owncloud_name'], false);
$this->emitAssign($newId);
}
} catch (Exception $e) {
$this->logger->error('Failed to shorten owncloud_name "{oldId}" to "{newId}" (UUID: "{uuid}" of {table})',
[
'app' => 'user_ldap',
'oldId' => $row['owncloud_name'],
'newId' => $newId,
'uuid' => $row['directory_uuid'],
'table' => $table,
'exception' => $e,
]
);
}
}
$result->closeCursor();
}
protected function getSelectQuery(string $table): IQueryBuilder {
$qb = $this->dbc->getQueryBuilder();
$qb->select('owncloud_name', 'directory_uuid')
->from($table)
->where($qb->expr()->gt($qb->func()->octetLength('owncloud_name'), $qb->createNamedParameter('64'), IQueryBuilder::PARAM_INT));
return $qb;
}
protected function getUpdateQuery(string $table): IQueryBuilder {
$qb = $this->dbc->getQueryBuilder();
$qb->update($table)
->set('owncloud_name', $qb->createParameter('newId'))
->where($qb->expr()->eq('directory_uuid', $qb->createParameter('uuid')));
return $qb;
}
protected function emitUnassign(string $oldId, bool $pre): void {
if ($this->userManager instanceof PublicEmitter) {
$this->userManager->emit('\OC\User', $pre ? 'pre' : 'post' . 'UnassignedUserId', [$oldId]);
}
}
protected function emitAssign(string $newId): void {
if ($this->userManager instanceof PublicEmitter) {
$this->userManager->emit('\OC\User', 'assignedUserId', [$newId]);
}
}
}
@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Migration;
use Closure;
use Generator;
use OCP\DB\Exception;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Psr\Log\LoggerInterface;
class Version1130Date20211102154716 extends SimpleMigrationStep {
/** @var IDBConnection */
private $dbc;
/** @var LoggerInterface */
private $logger;
/** @var string[] */
private $hashColumnAddedToTables = [];
public function __construct(IDBConnection $dbc, LoggerInterface $logger) {
$this->dbc = $dbc;
$this->logger = $logger;
}
public function getName() {
return 'Adjust LDAP user and group ldap_dn column lengths and add ldap_dn_hash columns';
}
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
$this->processDuplicateUUIDs($tableName);
}
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('ldap_group_mapping_backup')) {
// Previous upgrades of a broken release might have left an incomplete
// ldap_group_mapping_backup table. No need to recreate, but it
// should be empty.
// TRUNCATE is not available from Query Builder, but faster than DELETE FROM.
$sql = $this->dbc->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*ldap_group_mapping_backup`', false);
$this->dbc->executeStatement($sql);
}
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changeSchema = false;
foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
$table = $schema->getTable($tableName);
if (!$table->hasColumn('ldap_dn_hash')) {
$table->addColumn('ldap_dn_hash', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$changeSchema = true;
$this->hashColumnAddedToTables[] = $tableName;
}
$column = $table->getColumn('ldap_dn');
if ($tableName === 'ldap_user_mapping') {
if ($column->getLength() < 4000) {
$column->setLength(4000);
$changeSchema = true;
}
if ($table->hasIndex('ldap_dn_users')) {
$table->dropIndex('ldap_dn_users');
$changeSchema = true;
}
if (!$table->hasIndex('ldap_user_dn_hashes')) {
$table->addUniqueIndex(['ldap_dn_hash'], 'ldap_user_dn_hashes');
$changeSchema = true;
}
if (!$table->hasIndex('ldap_user_directory_uuid')) {
$table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid');
$changeSchema = true;
}
} elseif (!$schema->hasTable('ldap_group_mapping_backup')) {
// We need to copy the table twice to be able to change primary key, prepare the backup table
$table2 = $schema->createTable('ldap_group_mapping_backup');
$table2->addColumn('ldap_dn', Types::STRING, [
'notnull' => true,
'length' => 4000,
'default' => '',
]);
$table2->addColumn('owncloud_name', Types::STRING, [
'notnull' => true,
'length' => 64,
'default' => '',
]);
$table2->addColumn('directory_uuid', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table2->addColumn('ldap_dn_hash', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table2->setPrimaryKey(['owncloud_name'], 'lgm_backup_primary');
$changeSchema = true;
}
}
return $changeSchema ? $schema : null;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
$this->handleDNHashes('ldap_group_mapping');
$this->handleDNHashes('ldap_user_mapping');
}
protected function handleDNHashes(string $table): void {
$select = $this->getSelectQuery($table);
$update = $this->getUpdateQuery($table);
$result = $select->executeQuery();
while ($row = $result->fetch()) {
$dnHash = hash('sha256', $row['ldap_dn'], false);
$update->setParameter('name', $row['owncloud_name']);
$update->setParameter('dn_hash', $dnHash);
try {
$update->executeStatement();
} catch (Exception $e) {
$this->logger->error('Failed to add hash "{dnHash}" ("{name}" of {table})',
[
'app' => 'user_ldap',
'name' => $row['owncloud_name'],
'dnHash' => $dnHash,
'table' => $table,
'exception' => $e,
]
);
}
}
$result->closeCursor();
}
protected function getSelectQuery(string $table): IQueryBuilder {
$qb = $this->dbc->getQueryBuilder();
$qb->select('owncloud_name', 'ldap_dn')
->from($table);
// when added we may run into risk that it's read from a DB node
// where the column is not present. Then the where clause is also
// not necessary since all rows qualify.
if (!in_array($table, $this->hashColumnAddedToTables, true)) {
$qb->where($qb->expr()->isNull('ldap_dn_hash'));
}
return $qb;
}
protected function getUpdateQuery(string $table): IQueryBuilder {
$qb = $this->dbc->getQueryBuilder();
$qb->update($table)
->set('ldap_dn_hash', $qb->createParameter('dn_hash'))
->where($qb->expr()->eq('owncloud_name', $qb->createParameter('name')));
return $qb;
}
/**
* @throws Exception
*/
protected function processDuplicateUUIDs(string $table): void {
$uuids = $this->getDuplicatedUuids($table);
$idsWithUuidToInvalidate = [];
foreach ($uuids as $uuid) {
array_push($idsWithUuidToInvalidate, ...$this->getNextcloudIdsByUuid($table, $uuid));
}
$this->invalidateUuids($table, $idsWithUuidToInvalidate);
}
/**
* @throws Exception
*/
protected function invalidateUuids(string $table, array $idList): void {
$update = $this->dbc->getQueryBuilder();
$update->update($table)
->set('directory_uuid', $update->createParameter('invalidatedUuid'))
->where($update->expr()->eq('owncloud_name', $update->createParameter('nextcloudId')));
while ($nextcloudId = array_shift($idList)) {
$update->setParameter('nextcloudId', $nextcloudId);
$update->setParameter('invalidatedUuid', 'invalidated_' . \bin2hex(\random_bytes(6)));
try {
$update->executeStatement();
$this->logger->warning(
'LDAP user or group with ID {nid} has a duplicated UUID value which therefore was invalidated. You may double-check your LDAP configuration and trigger an update of the UUID.',
[
'app' => 'user_ldap',
'nid' => $nextcloudId,
]
);
} catch (Exception $e) {
// Catch possible, but unlikely duplications if new invalidated errors.
// There is the theoretical chance of an infinity loop is, when
// the constraint violation has a different background. I cannot
// think of one at the moment.
if ($e->getReason() !== Exception::REASON_CONSTRAINT_VIOLATION) {
throw $e;
}
$idList[] = $nextcloudId;
}
}
}
/**
* @throws \OCP\DB\Exception
* @return array<string>
*/
protected function getNextcloudIdsByUuid(string $table, string $uuid): array {
$select = $this->dbc->getQueryBuilder();
$select->select('owncloud_name')
->from($table)
->where($select->expr()->eq('directory_uuid', $select->createNamedParameter($uuid)));
$result = $select->executeQuery();
$idList = [];
while (($id = $result->fetchOne()) !== false) {
$idList[] = $id;
}
$result->closeCursor();
return $idList;
}
/**
* @return Generator<string>
* @throws \OCP\DB\Exception
*/
protected function getDuplicatedUuids(string $table): Generator {
$select = $this->dbc->getQueryBuilder();
$select->select('directory_uuid')
->from($table)
->groupBy('directory_uuid')
->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1)));
$result = $select->executeQuery();
while (($uuid = $result->fetchOne()) !== false) {
yield $uuid;
}
$result->closeCursor();
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
class Version1130Date20220110154717 extends GroupMappingMigration {
public function getName() {
return 'Copy ldap_group_mapping data to backup table if needed';
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @since 13.0.0
*/
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_mapping_backup')) {
// Backup table does not exist
return;
}
$output->startProgress();
$this->copyGroupMappingData('ldap_group_mapping', 'ldap_group_mapping_backup');
$output->finishProgress();
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_mapping_backup')) {
// Backup table does not exist
return null;
}
$schema->dropTable('ldap_group_mapping');
return $schema;
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
class Version1130Date20220110154718 extends GroupMappingMigration {
public function getName() {
return 'Copy ldap_group_mapping data from backup table and if needed';
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_mapping_backup')) {
// Backup table does not exist
return null;
}
$table = $schema->createTable('ldap_group_mapping');
$table->addColumn('ldap_dn', Types::STRING, [
'notnull' => true,
'length' => 4000,
'default' => '',
]);
$table->addColumn('owncloud_name', Types::STRING, [
'notnull' => true,
'length' => 64,
'default' => '',
]);
$table->addColumn('directory_uuid', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('ldap_dn_hash', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->setPrimaryKey(['owncloud_name']);
$table->addUniqueIndex(['ldap_dn_hash'], 'ldap_group_dn_hashes');
$table->addUniqueIndex(['directory_uuid'], 'ldap_group_directory_uuid');
return $schema;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_mapping_backup')) {
// Backup table does not exist
return;
}
$output->startProgress();
$this->copyGroupMappingData('ldap_group_mapping_backup', 'ldap_group_mapping');
$output->finishProgress();
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1130Date20220110154719 extends SimpleMigrationStep {
public function getName() {
return 'Drop ldap_group_mapping_backup';
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('ldap_group_mapping_backup')) {
$schema->dropTable('ldap_group_mapping_backup');
return $schema;
}
return null;
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1141Date20220323143801 extends SimpleMigrationStep {
private IDBConnection $dbc;
public function __construct(IDBConnection $dbc) {
$this->dbc = $dbc;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
$qb = $this->dbc->getQueryBuilder();
$qb->select('ldap_dn')
->from($tableName)
->where($qb->expr()->gt($qb->func()->octetLength('ldap_dn'), $qb->createNamedParameter('4000'), IQueryBuilder::PARAM_INT));
$dnsTooLong = [];
$result = $qb->executeQuery();
while (($dn = $result->fetchOne()) !== false) {
$dnsTooLong[] = $dn;
}
$result->closeCursor();
$this->shortenDNs($dnsTooLong, $tableName);
}
}
protected function shortenDNs(array $dns, string $table): void {
$qb = $this->dbc->getQueryBuilder();
$qb->update($table)
->set('ldap_dn', $qb->createParameter('shortenedDn'))
->where($qb->expr()->eq('ldap_dn', $qb->createParameter('originalDn')));
$pageSize = 1000;
$page = 0;
do {
$subset = array_slice($dns, $page * $pageSize, $pageSize);
try {
$this->dbc->beginTransaction();
foreach ($subset as $dn) {
$shortenedDN = mb_substr($dn, 0, 4000);
$qb->setParameter('shortenedDn', $shortenedDN);
$qb->setParameter('originalDn', $dn);
$qb->executeStatement();
}
$this->dbc->commit();
} catch (\Throwable $t) {
$this->dbc->rollBack();
throw $t;
}
$page++;
} while (count($subset) === $pageSize);
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
$table = $schema->getTable($tableName);
$column = $table->getColumn('ldap_dn');
if ($column->getLength() > 4000) {
$column->setLength(4000);
}
}
return $schema;
}
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Your name <your@email.com>
*
* @author Your name <your@email.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1190Date20230706134108 extends SimpleMigrationStep {
public function __construct(
private IDBConnection $dbc,
) {
}
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_membership')) {
$table = $schema->createTable('ldap_group_membership');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('groupid', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('userid', Types::STRING, [
'notnull' => true,
'length' => 64,
'default' => '',
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['groupid', 'userid'], 'user_ldap_membership_unique');
return $schema;
} else {
return null;
}
}
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('ldap_group_members')) {
// Old table does not exist
return;
}
$output->startProgress();
$this->copyGroupMembershipData();
$output->finishProgress();
}
protected function copyGroupMembershipData(): void {
$insert = $this->dbc->getQueryBuilder();
$insert->insert('ldap_group_membership')
->values([
'userid' => $insert->createParameter('userid'),
'groupid' => $insert->createParameter('groupid'),
]);
$query = $this->dbc->getQueryBuilder();
$query->select('*')
->from('ldap_group_members');
$result = $query->executeQuery();
while ($row = $result->fetch()) {
$knownUsers = unserialize($row['owncloudusers']);
$knownUsers = array_unique($knownUsers);
foreach ($knownUsers as $knownUser) {
try {
$insert
->setParameter('groupid', $row['owncloudname'])
->setParameter('userid', $knownUser)
;
$insert->executeStatement();
} catch (\OCP\DB\Exception $e) {
/*
* If it fails on unique constaint violation it may just be left over value from previous half-migration
* If it fails on something else, ignore as well, data will be filled by background job later anyway
*/
}
}
}
$result->closeCursor();
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Your name <your@email.com>
*
* @author Your name <your@email.com>
*
* @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\User_LDAP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1190Date20230706134109 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('ldap_group_members')) {
$schema->dropTable('ldap_group_members');
return $schema;
}
return null;
}
}
@@ -0,0 +1,102 @@
<?php
/**
* @copyright Copyright (c) 2017 Roger Szabo <roger.szabo@web.de>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
*
* @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\User_LDAP\Notification;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class Notifier implements INotifier {
/** @var IFactory */
protected $l10nFactory;
/**
* @param IFactory $l10nFactory
*/
public function __construct(\OCP\L10N\IFactory $l10nFactory) {
$this->l10nFactory = $l10nFactory;
}
/**
* Identifier of the notifier, only use [a-z0-9_]
*
* @return string
* @since 17.0.0
*/
public function getID(): string {
return 'user_ldap';
}
/**
* Human readable name describing the notifier
*
* @return string
* @since 17.0.0
*/
public function getName(): string {
return $this->l10nFactory->get('user_ldap')->t('LDAP User backend');
}
/**
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
* @throws \InvalidArgumentException When the notification was not prepared by a notifier
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== 'user_ldap') {
// Not my app => throw
throw new \InvalidArgumentException();
}
// Read the language from the notification
$l = $this->l10nFactory->get('user_ldap', $languageCode);
switch ($notification->getSubject()) {
// Deal with known subjects
case 'pwd_exp_warn_days':
$params = $notification->getSubjectParameters();
$days = (int) $params[0];
if ($days === 2) {
$notification->setParsedSubject($l->t('Your password will expire tomorrow.'));
} elseif ($days === 1) {
$notification->setParsedSubject($l->t('Your password will expire today.'));
} else {
$notification->setParsedSubject($l->n(
'Your password will expire within %n day.',
'Your password will expire within %n days.',
$days
));
}
return $notification;
default:
// Unknown subject => Unknown notification => throw
throw new \InvalidArgumentException();
}
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\User_LDAP\PagedResults;
trait TLinkId {
public function getLinkId($link) {
if (is_object($link)) {
return spl_object_id($link);
} elseif (is_resource($link)) {
return (int)$link;
} elseif (is_array($link) && isset($link[0])) {
if (is_object($link[0])) {
return spl_object_id($link[0]);
} elseif (is_resource($link[0])) {
return (int)$link[0];
}
}
throw new \RuntimeException('No resource provided');
}
}
+194
View File
@@ -0,0 +1,194 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OCA\User_LDAP\Mapping\GroupMapping;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\ICache;
use OCP\Server;
abstract class Proxy {
/** @var array<string,Access> */
private static array $accesses = [];
private ILDAPWrapper $ldap;
private ?bool $isSingleBackend = null;
private ?ICache $cache = null;
private AccessFactory $accessFactory;
public function __construct(
ILDAPWrapper $ldap,
AccessFactory $accessFactory
) {
$this->ldap = $ldap;
$this->accessFactory = $accessFactory;
$memcache = \OC::$server->getMemCacheFactory();
if ($memcache->isAvailable()) {
$this->cache = $memcache->createDistributed();
}
}
private function addAccess(string $configPrefix): void {
$userMap = Server::get(UserMapping::class);
$groupMap = Server::get(GroupMapping::class);
$connector = new Connection($this->ldap, $configPrefix);
$access = $this->accessFactory->get($connector);
$access->setUserMapper($userMap);
$access->setGroupMapper($groupMap);
self::$accesses[$configPrefix] = $access;
}
protected function getAccess(string $configPrefix): Access {
if (!isset(self::$accesses[$configPrefix])) {
$this->addAccess($configPrefix);
}
return self::$accesses[$configPrefix];
}
/**
* @param string $uid
* @return string
*/
protected function getUserCacheKey($uid) {
return 'user-' . $uid . '-lastSeenOn';
}
/**
* @param string $gid
* @return string
*/
protected function getGroupCacheKey($gid) {
return 'group-' . $gid . '-lastSeenOn';
}
/**
* @param string $id
* @param string $method
* @param array $parameters
* @param bool $passOnWhen
* @return mixed
*/
abstract protected function callOnLastSeenOn($id, $method, $parameters, $passOnWhen);
/**
* @param string $id
* @param string $method
* @param array $parameters
* @return mixed
*/
abstract protected function walkBackends($id, $method, $parameters);
/**
* @param string $id
* @return Access
*/
abstract public function getLDAPAccess($id);
abstract protected function activeBackends(): int;
protected function isSingleBackend(): bool {
if ($this->isSingleBackend === null) {
$this->isSingleBackend = $this->activeBackends() === 1;
}
return $this->isSingleBackend;
}
/**
* Takes care of the request to the User backend
*
* @param string $id
* @param string $method string, the method of the user backend that shall be called
* @param array $parameters an array of parameters to be passed
* @param bool $passOnWhen
* @return mixed the result of the specified method
*/
protected function handleRequest($id, $method, $parameters, $passOnWhen = false) {
if (!$this->isSingleBackend()) {
$result = $this->callOnLastSeenOn($id, $method, $parameters, $passOnWhen);
}
if (!isset($result) || $result === $passOnWhen) {
$result = $this->walkBackends($id, $method, $parameters);
}
return $result;
}
/**
* @param string|null $key
* @return string
*/
private function getCacheKey($key) {
$prefix = 'LDAP-Proxy-';
if ($key === null) {
return $prefix;
}
return $prefix . hash('sha256', $key);
}
/**
* @param string $key
* @return mixed|null
*/
public function getFromCache($key) {
if ($this->cache === null) {
return null;
}
$key = $this->getCacheKey($key);
$value = $this->cache->get($key);
if ($value === null) {
return null;
}
return json_decode(base64_decode($value));
}
/**
* @param string $key
* @param mixed $value
*/
public function writeToCache($key, $value) {
if ($this->cache === null) {
return;
}
$key = $this->getCacheKey($key);
$value = base64_encode(json_encode($value));
$this->cache->set($key, $value, 2592000);
}
public function clearCache() {
if ($this->cache === null) {
return;
}
$this->cache->clear($this->getCacheKey(null));
}
}
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Service;
use OCA\User_LDAP\Db\GroupMembership;
use OCA\User_LDAP\Db\GroupMembershipMapper;
use OCA\User_LDAP\Group_Proxy;
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class UpdateGroupsService {
public function __construct(
private Group_Proxy $groupBackend,
private IEventDispatcher $dispatcher,
private IGroupManager $groupManager,
private IUserManager $userManager,
private LoggerInterface $logger,
private GroupMembershipMapper $groupMembershipMapper,
) {
}
/**
* @throws Exception
*/
public function updateGroups(): void {
$knownGroups = $this->groupMembershipMapper->getKnownGroups();
$actualGroups = $this->groupBackend->getGroups();
if (empty($actualGroups) && empty($knownGroups)) {
$this->logger->info(
'service "updateGroups" – groups do not seem to be configured properly, aborting.',
);
return;
}
$this->handleKnownGroups(array_intersect($actualGroups, $knownGroups));
$this->handleCreatedGroups(array_diff($actualGroups, $knownGroups));
$this->handleRemovedGroups(array_diff($knownGroups, $actualGroups));
$this->logger->debug('service "updateGroups" – Finished.');
}
/**
* @param string[] $groups
* @throws Exception
*/
public function handleKnownGroups(array $groups): void {
$this->logger->debug('service "updateGroups" – Dealing with known Groups.');
foreach ($groups as $group) {
$this->logger->debug('service "updateGroups" – Dealing with {group}.', ['group' => $group]);
$groupMemberships = $this->groupMembershipMapper->findGroupMemberships($group);
$knownUsers = array_map(
static fn (GroupMembership $groupMembership): string => $groupMembership->getUserid(),
$groupMemberships
);
$groupMemberships = array_combine($knownUsers, $groupMemberships);
$actualUsers = $this->groupBackend->usersInGroup($group);
$groupObject = $this->groupManager->get($group);
if ($groupObject === null) {
/* We are not expecting the group to not be found since it was returned by $this->groupBackend->getGroups() */
$this->logger->error(
'service "updateGroups" – Failed to get group {group} for update',
[
'group' => $group
]
);
continue;
}
foreach (array_diff($knownUsers, $actualUsers) as $removedUser) {
$this->groupMembershipMapper->delete($groupMemberships[$removedUser]);
$userObject = $this->userManager->get($removedUser);
if ($userObject instanceof IUser) {
$this->dispatcher->dispatchTyped(new UserRemovedEvent($groupObject, $userObject));
}
$this->logger->info(
'service "updateGroups" – {user} removed from {group}',
[
'user' => $removedUser,
'group' => $group
]
);
}
foreach (array_diff($actualUsers, $knownUsers) as $addedUser) {
$this->groupMembershipMapper->insert(GroupMembership::fromParams(['groupid' => $group,'userid' => $addedUser]));
$userObject = $this->userManager->get($addedUser);
if ($userObject instanceof IUser) {
$this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject));
}
$this->logger->info(
'service "updateGroups" – {user} added to {group}',
[
'user' => $addedUser,
'group' => $group
]
);
}
}
$this->logger->debug('service "updateGroups" – FINISHED dealing with known Groups.');
}
/**
* @param string[] $createdGroups
* @throws Exception
*/
public function handleCreatedGroups(array $createdGroups): void {
$this->logger->debug('service "updateGroups" – dealing with created Groups.');
foreach ($createdGroups as $createdGroup) {
$this->logger->info('service "updateGroups" – new group "' . $createdGroup . '" found.');
$users = $this->groupBackend->usersInGroup($createdGroup);
$groupObject = $this->groupManager->get($createdGroup);
foreach ($users as $user) {
$this->groupMembershipMapper->insert(GroupMembership::fromParams(['groupid' => $createdGroup,'userid' => $user]));
if ($groupObject instanceof IGroup) {
$userObject = $this->userManager->get($user);
if ($userObject instanceof IUser) {
$this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject));
}
}
}
}
$this->logger->debug('service "updateGroups" – FINISHED dealing with created Groups.');
}
/**
* @param string[] $removedGroups
* @throws Exception
*/
public function handleRemovedGroups(array $removedGroups): void {
$this->logger->debug('service "updateGroups" – dealing with removed groups.');
$this->groupMembershipMapper->deleteGroups($removedGroups);
foreach ($removedGroups as $group) {
$groupObject = $this->groupManager->get($group);
if ($groupObject instanceof IGroup) {
$groupMemberships = $this->groupMembershipMapper->findGroupMemberships($group);
foreach ($groupMemberships as $groupMembership) {
$userObject = $this->userManager->get($groupMembership->getUserid());
if ($userObject instanceof IUser) {
$this->dispatcher->dispatchTyped(new UserRemovedEvent($groupObject, $userObject));
}
}
}
}
$this->logger->info(
'service "updateGroups" – groups {removedGroups} were removed.',
[
'removedGroups' => $removedGroups
]
);
}
}
@@ -0,0 +1,109 @@
<?php
/**
* @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @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\User_LDAP\Settings;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\Helper;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
use OCP\Template;
class Admin implements IDelegatedSettings {
/** @var IL10N */
private $l;
/**
* @param IL10N $l
*/
public function __construct(IL10N $l) {
$this->l = $l;
}
/**
* @return TemplateResponse
*/
public function getForm() {
$helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
$prefixes = $helper->getServerConfigurationPrefixes();
if (count($prefixes) === 0) {
$newPrefix = $helper->getNextServerConfigurationPrefix();
$config = new Configuration($newPrefix, false);
$config->setConfiguration($config->getDefaults());
$config->saveConfiguration();
$prefixes[] = $newPrefix;
}
$hosts = $helper->getServerConfigurationHosts();
$wControls = new Template('user_ldap', 'part.wizardcontrols');
$wControls = $wControls->fetchPage();
$sControls = new Template('user_ldap', 'part.settingcontrols');
$sControls = $sControls->fetchPage();
$parameters['serverConfigurationPrefixes'] = $prefixes;
$parameters['serverConfigurationHosts'] = $hosts;
$parameters['settingControls'] = $sControls;
$parameters['wizardControls'] = $wControls;
// assign default values
if (!isset($config)) {
$config = new Configuration('', false);
}
$defaults = $config->getDefaults();
foreach ($defaults as $key => $default) {
$parameters[$key.'_default'] = $default;
}
return new TemplateResponse('user_ldap', 'settings', $parameters);
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
return 'ldap';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority() {
return 5;
}
public function getName(): ?string {
return null; // Only one setting in this section
}
public function getAuthorizedAppConfig(): array {
return []; // Custom controller
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Valdnet <47037905+Valdnet@users.noreply.github.com>
*
* @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\User_LDAP\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class Section implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $url;
/**
* @param IURLGenerator $url
* @param IL10N $l
*/
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
*/
public function getID() {
return 'ldap';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP/AD
* integration'. Use the L10N service to translate it.
*
* @return string
*/
public function getName() {
return $this->l->t('LDAP/AD integration');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
*/
public function getPriority() {
return 25;
}
/**
* {@inheritdoc}
*/
public function getIcon() {
return $this->url->imagePath('user_ldap', 'app-dark.svg');
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\User_LDAP\SetupChecks;
use OCA\User_LDAP\Mapping\GroupMapping;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
class LdapInvalidUuids implements ISetupCheck {
public function __construct(
private IL10N $l10n,
private UserMapping $userMapping,
private GroupMapping $groupMapping,
) {
}
public function getCategory(): string {
return 'ldap';
}
public function getName(): string {
return $this->l10n->t('Invalid LDAP UUIDs');
}
public function run(): SetupResult {
if (count($this->userMapping->getList(0, 1, true)) === 0
&& count($this->groupMapping->getList(0, 1, true)) === 0) {
return SetupResult::success($this->l10n->t('None found'));
} else {
return SetupResult::warning($this->l10n->t('Invalid UUIDs of LDAP users or groups have been found. Please review your "Override UUID detection" settings in the Expert part of the LDAP configuration and use "occ ldap:update-uuid" to update them.'));
}
}
}
@@ -0,0 +1,105 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\User;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\IConfig;
use OCP\Share\IManager;
/**
* Class DeletedUsersIndex
* @package OCA\User_LDAP
*/
class DeletedUsersIndex {
protected IConfig $config;
protected UserMapping $mapping;
protected ?array $deletedUsers = null;
private IManager $shareManager;
public function __construct(
IConfig $config,
UserMapping $mapping,
IManager $shareManager
) {
$this->config = $config;
$this->mapping = $mapping;
$this->shareManager = $shareManager;
}
/**
* reads LDAP users marked as deleted from the database
* @return OfflineUser[]
*/
private function fetchDeletedUsers(): array {
$deletedUsers = $this->config->getUsersForUserValue('user_ldap', 'isDeleted', '1');
$userObjects = [];
foreach ($deletedUsers as $user) {
$userObjects[] = new OfflineUser($user, $this->config, $this->mapping, $this->shareManager);
}
$this->deletedUsers = $userObjects;
return $this->deletedUsers;
}
/**
* returns all LDAP users that are marked as deleted
* @return OfflineUser[]
*/
public function getUsers(): array {
if (is_array($this->deletedUsers)) {
return $this->deletedUsers;
}
return $this->fetchDeletedUsers();
}
/**
* whether at least one user was detected as deleted
*/
public function hasUsers(): bool {
if (!is_array($this->deletedUsers)) {
$this->fetchDeletedUsers();
}
return is_array($this->deletedUsers) && (count($this->deletedUsers) > 0);
}
/**
* marks a user as deleted
*
* @throws \OCP\PreConditionNotMetException
*/
public function markUser(string $ocName): void {
if ($this->isUserMarked($ocName)) {
// the user is already marked, do not write to DB again
return;
}
$this->config->setUserValue($ocName, 'user_ldap', 'isDeleted', '1');
$this->config->setUserValue($ocName, 'user_ldap', 'foundDeleted', (string)time());
$this->deletedUsers = null;
}
public function isUserMarked(string $ocName): bool {
return ($this->config->getUserValue($ocName, 'user_ldap', 'isDeleted', '0') === '1');
}
}
@@ -0,0 +1,261 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Marc Hefter <marchefter@march42.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Roger Szabo <roger.szabo@web.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\User;
use OCA\User_LDAP\Access;
use OCA\User_LDAP\FilesystemHelper;
use OCP\Cache\CappedMemoryCache;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Image;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
/**
* Manager
*
* upon request, returns an LDAP user object either by creating or from run-time
* cache
*/
class Manager {
protected ?Access $access = null;
protected IConfig $ocConfig;
protected IDBConnection $db;
protected IUserManager $userManager;
protected INotificationManager $notificationManager;
protected FilesystemHelper $ocFilesystem;
protected LoggerInterface $logger;
protected Image $image;
protected IAvatarManager $avatarManager;
/** @var CappedMemoryCache<User> $usersByDN */
protected CappedMemoryCache $usersByDN;
/** @var CappedMemoryCache<User> $usersByUid */
protected CappedMemoryCache $usersByUid;
private IManager $shareManager;
public function __construct(
IConfig $ocConfig,
FilesystemHelper $ocFilesystem,
LoggerInterface $logger,
IAvatarManager $avatarManager,
Image $image,
IUserManager $userManager,
INotificationManager $notificationManager,
IManager $shareManager
) {
$this->ocConfig = $ocConfig;
$this->ocFilesystem = $ocFilesystem;
$this->logger = $logger;
$this->avatarManager = $avatarManager;
$this->image = $image;
$this->userManager = $userManager;
$this->notificationManager = $notificationManager;
$this->usersByDN = new CappedMemoryCache();
$this->usersByUid = new CappedMemoryCache();
$this->shareManager = $shareManager;
}
/**
* Binds manager to an instance of Access.
* It needs to be assigned first before the manager can be used.
* @param Access
*/
public function setLdapAccess(Access $access) {
$this->access = $access;
}
/**
* @brief creates an instance of User and caches (just runtime) it in the
* property array
* @param string $dn the DN of the user
* @param string $uid the internal (owncloud) username
* @return \OCA\User_LDAP\User\User
*/
private function createAndCache($dn, $uid) {
$this->checkAccess();
$user = new User($uid, $dn, $this->access, $this->ocConfig,
$this->ocFilesystem, clone $this->image, $this->logger,
$this->avatarManager, $this->userManager,
$this->notificationManager);
$this->usersByDN[$dn] = $user;
$this->usersByUid[$uid] = $user;
return $user;
}
/**
* removes a user entry from the cache
* @param $uid
*/
public function invalidate($uid) {
if (!isset($this->usersByUid[$uid])) {
return;
}
$dn = $this->usersByUid[$uid]->getDN();
unset($this->usersByUid[$uid]);
unset($this->usersByDN[$dn]);
}
/**
* @brief checks whether the Access instance has been set
* @throws \Exception if Access has not been set
* @return null
*/
private function checkAccess() {
if (is_null($this->access)) {
throw new \Exception('LDAP Access instance must be set first');
}
}
/**
* returns a list of attributes that will be processed further, e.g. quota,
* email, displayname, or others.
*
* @param bool $minimal - optional, set to true to skip attributes with big
* payload
* @return string[]
*/
public function getAttributes($minimal = false) {
$baseAttributes = array_merge(Access::UUID_ATTRIBUTES, ['dn', 'uid', 'samaccountname', 'memberof']);
$attributes = [
$this->access->getConnection()->ldapExpertUUIDUserAttr,
$this->access->getConnection()->ldapQuotaAttribute,
$this->access->getConnection()->ldapEmailAttribute,
$this->access->getConnection()->ldapUserDisplayName,
$this->access->getConnection()->ldapUserDisplayName2,
$this->access->getConnection()->ldapExtStorageHomeAttribute,
$this->access->getConnection()->ldapAttributePhone,
$this->access->getConnection()->ldapAttributeWebsite,
$this->access->getConnection()->ldapAttributeAddress,
$this->access->getConnection()->ldapAttributeTwitter,
$this->access->getConnection()->ldapAttributeFediverse,
$this->access->getConnection()->ldapAttributeOrganisation,
$this->access->getConnection()->ldapAttributeRole,
$this->access->getConnection()->ldapAttributeHeadline,
$this->access->getConnection()->ldapAttributeBiography,
];
$homeRule = (string)$this->access->getConnection()->homeFolderNamingRule;
if (str_starts_with($homeRule, 'attr:')) {
$attributes[] = substr($homeRule, strlen('attr:'));
}
if (!$minimal) {
// attributes that are not really important but may come with big
// payload.
$attributes = array_merge(
$attributes,
$this->access->getConnection()->resolveRule('avatar')
);
}
$attributes = array_reduce($attributes,
function ($list, $attribute) {
$attribute = strtolower(trim((string)$attribute));
if (!empty($attribute) && !in_array($attribute, $list)) {
$list[] = $attribute;
}
return $list;
},
$baseAttributes // hard-coded, lower-case, non-empty attributes
);
return $attributes;
}
/**
* Checks whether the specified user is marked as deleted
* @param string $id the Nextcloud user name
* @return bool
*/
public function isDeletedUser($id) {
$isDeleted = $this->ocConfig->getUserValue(
$id, 'user_ldap', 'isDeleted', 0);
return (int)$isDeleted === 1;
}
/**
* creates and returns an instance of OfflineUser for the specified user
* @param string $id
* @return \OCA\User_LDAP\User\OfflineUser
*/
public function getDeletedUser($id) {
return new OfflineUser(
$id,
$this->ocConfig,
$this->access->getUserMapper(),
$this->shareManager
);
}
/**
* @brief returns a User object by its Nextcloud username
* @param string $id the DN or username of the user
* @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
*/
protected function createInstancyByUserName($id) {
//most likely a uid. Check whether it is a deleted user
if ($this->isDeletedUser($id)) {
return $this->getDeletedUser($id);
}
$dn = $this->access->username2dn($id);
if ($dn !== false) {
return $this->createAndCache($dn, $id);
}
return null;
}
/**
* @brief returns a User object by its DN or Nextcloud username
* @param string $id the DN or username of the user
* @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
* @throws \Exception when connection could not be established
*/
public function get($id) {
$this->checkAccess();
if (isset($this->usersByDN[$id])) {
return $this->usersByDN[$id];
} elseif (isset($this->usersByUid[$id])) {
return $this->usersByUid[$id];
}
if ($this->access->stringResemblesDN($id)) {
$uid = $this->access->dn2username($id);
if ($uid !== false) {
return $this->createAndCache($id, $uid);
}
}
return $this->createInstancyByUserName($id);
}
}
@@ -0,0 +1,276 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\User;
use OCA\User_LDAP\Mapping\UserMapping;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Share\IManager;
use OCP\Share\IShare;
class OfflineUser {
/**
* @var string $ocName
*/
protected $ocName;
/**
* @var string $dn
*/
protected $dn;
/**
* @var string $uid the UID as provided by LDAP
*/
protected $uid;
/**
* @var string $displayName
*/
protected $displayName;
/**
* @var string $homePath
*/
protected $homePath;
/**
* @var string $lastLogin the timestamp of the last login
*/
protected $lastLogin;
/**
* @var string $foundDeleted the timestamp when the user was detected as unavailable
*/
protected $foundDeleted;
protected ?string $extStorageHome = null;
/**
* @var string $email
*/
protected $email;
/**
* @var bool $hasActiveShares
*/
protected $hasActiveShares;
/**
* @var IConfig $config
*/
protected $config;
/**
* @var IDBConnection $db
*/
protected $db;
/**
* @var \OCA\User_LDAP\Mapping\UserMapping
*/
protected $mapping;
/** @var IManager */
private $shareManager;
public function __construct(
$ocName,
IConfig $config,
UserMapping $mapping,
IManager $shareManager
) {
$this->ocName = $ocName;
$this->config = $config;
$this->mapping = $mapping;
$this->shareManager = $shareManager;
}
/**
* remove the Delete-flag from the user.
*/
public function unmark() {
$this->config->deleteUserValue($this->ocName, 'user_ldap', 'isDeleted');
$this->config->deleteUserValue($this->ocName, 'user_ldap', 'foundDeleted');
}
/**
* exports the user details in an assoc array
* @return array
*/
public function export() {
$data = [];
$data['ocName'] = $this->getOCName();
$data['dn'] = $this->getDN();
$data['uid'] = $this->getUID();
$data['displayName'] = $this->getDisplayName();
$data['homePath'] = $this->getHomePath();
$data['lastLogin'] = $this->getLastLogin();
$data['email'] = $this->getEmail();
$data['hasActiveShares'] = $this->getHasActiveShares();
return $data;
}
/**
* getter for Nextcloud internal name
* @return string
*/
public function getOCName() {
return $this->ocName;
}
/**
* getter for LDAP uid
* @return string
*/
public function getUID() {
if ($this->uid === null) {
$this->fetchDetails();
}
return $this->uid;
}
/**
* getter for LDAP DN
* @return string
*/
public function getDN() {
if ($this->dn === null) {
$dn = $this->mapping->getDNByName($this->ocName);
$this->dn = ($dn !== false) ? $dn : '';
}
return $this->dn;
}
/**
* getter for display name
* @return string
*/
public function getDisplayName() {
if ($this->displayName === null) {
$this->fetchDetails();
}
return $this->displayName;
}
/**
* getter for email
* @return string
*/
public function getEmail() {
if ($this->email === null) {
$this->fetchDetails();
}
return $this->email;
}
/**
* getter for home directory path
* @return string
*/
public function getHomePath() {
if ($this->homePath === null) {
$this->fetchDetails();
}
return $this->homePath;
}
/**
* getter for the last login timestamp
* @return int
*/
public function getLastLogin() {
if ($this->lastLogin === null) {
$this->fetchDetails();
}
return (int)$this->lastLogin;
}
/**
* getter for the detection timestamp
* @return int
*/
public function getDetectedOn() {
if ($this->foundDeleted === null) {
$this->fetchDetails();
}
return (int)$this->foundDeleted;
}
public function getExtStorageHome(): string {
if ($this->extStorageHome === null) {
$this->fetchDetails();
}
return (string)$this->extStorageHome;
}
/**
* getter for having active shares
* @return bool
*/
public function getHasActiveShares() {
if ($this->hasActiveShares === null) {
$this->determineShares();
}
return $this->hasActiveShares;
}
/**
* reads the user details
*/
protected function fetchDetails() {
$properties = [
'displayName' => 'user_ldap',
'uid' => 'user_ldap',
'homePath' => 'user_ldap',
'foundDeleted' => 'user_ldap',
'extStorageHome' => 'user_ldap',
'email' => 'settings',
'lastLogin' => 'login',
];
foreach ($properties as $property => $app) {
$this->$property = $this->config->getUserValue($this->ocName, $app, $property, '');
}
}
/**
* finds out whether the user has active shares. The result is stored in
* $this->hasActiveShares
*/
protected function determineShares() {
$shareInterface = new \ReflectionClass(IShare::class);
$shareConstants = $shareInterface->getConstants();
foreach ($shareConstants as $constantName => $constantValue) {
if (!str_starts_with($constantName, 'TYPE_')
|| $constantValue === IShare::TYPE_USERGROUP
) {
continue;
}
$shares = $this->shareManager->getSharesBy(
$this->ocName,
$constantValue,
null,
false,
1
);
if (!empty($shares)) {
$this->hasActiveShares = true;
return;
}
}
$this->hasActiveShares = false;
}
}
@@ -0,0 +1,896 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
* @author Marc Hefter <marchefter@march42.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Philipp Staiger <philipp@staiger.it>
* @author Roger Szabo <roger.szabo@web.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\User;
use OC\Accounts\AccountManager;
use OCA\User_LDAP\Access;
use OCA\User_LDAP\Connection;
use OCA\User_LDAP\Exceptions\AttributeNotSet;
use OCA\User_LDAP\FilesystemHelper;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\Image;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Server;
use Psr\Log\LoggerInterface;
/**
* User
*
* represents an LDAP user, gets and holds user-specific information from LDAP
*/
class User {
/**
* @var Access
*/
protected $access;
/**
* @var Connection
*/
protected $connection;
/**
* @var IConfig
*/
protected $config;
/**
* @var FilesystemHelper
*/
protected $fs;
/**
* @var Image
*/
protected $image;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var IAvatarManager
*/
protected $avatarManager;
/**
* @var IUserManager
*/
protected $userManager;
/**
* @var INotificationManager
*/
protected $notificationManager;
/**
* @var string
*/
protected $dn;
/**
* @var string
*/
protected $uid;
/**
* @var string[]
*/
protected $refreshedFeatures = [];
/**
* @var string
*/
protected $avatarImage;
/**
* DB config keys for user preferences
*/
public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished';
/**
* @brief constructor, make sure the subclasses call this one!
* @param string $username the internal username
* @param string $dn the LDAP DN
*/
public function __construct($username, $dn, Access $access,
IConfig $config, FilesystemHelper $fs, Image $image,
LoggerInterface $logger, IAvatarManager $avatarManager, IUserManager $userManager,
INotificationManager $notificationManager) {
if ($username === null) {
$logger->error("uid for '$dn' must not be null!", ['app' => 'user_ldap']);
throw new \InvalidArgumentException('uid must not be null!');
} elseif ($username === '') {
$logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']);
throw new \InvalidArgumentException('uid must not be an empty string!');
}
$this->access = $access;
$this->connection = $access->getConnection();
$this->config = $config;
$this->fs = $fs;
$this->dn = $dn;
$this->uid = $username;
$this->image = $image;
$this->logger = $logger;
$this->avatarManager = $avatarManager;
$this->userManager = $userManager;
$this->notificationManager = $notificationManager;
\OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
}
/**
* marks a user as deleted
*
* @throws \OCP\PreConditionNotMetException
*/
public function markUser() {
$curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0');
if ($curValue === '1') {
// the user is already marked, do not write to DB again
return;
}
$this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1');
$this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time());
}
/**
* processes results from LDAP for attributes as returned by getAttributesToRead()
* @param array $ldapEntry the user entry as retrieved from LDAP
*/
public function processAttributes($ldapEntry) {
//Quota
$attr = strtolower($this->connection->ldapQuotaAttribute);
if (isset($ldapEntry[$attr])) {
$this->updateQuota($ldapEntry[$attr][0]);
} else {
if ($this->connection->ldapQuotaDefault !== '') {
$this->updateQuota();
}
}
unset($attr);
//displayName
$displayName = $displayName2 = '';
$attr = strtolower($this->connection->ldapUserDisplayName);
if (isset($ldapEntry[$attr])) {
$displayName = (string)$ldapEntry[$attr][0];
}
$attr = strtolower($this->connection->ldapUserDisplayName2);
if (isset($ldapEntry[$attr])) {
$displayName2 = (string)$ldapEntry[$attr][0];
}
if ($displayName !== '') {
$this->composeAndStoreDisplayName($displayName, $displayName2);
$this->access->cacheUserDisplayName(
$this->getUsername(),
$displayName,
$displayName2
);
}
unset($attr);
//Email
//email must be stored after displayname, because it would cause a user
//change event that will trigger fetching the display name again
$attr = strtolower($this->connection->ldapEmailAttribute);
if (isset($ldapEntry[$attr])) {
$this->updateEmail($ldapEntry[$attr][0]);
}
unset($attr);
// LDAP Username, needed for s2s sharing
if (isset($ldapEntry['uid'])) {
$this->storeLDAPUserName($ldapEntry['uid'][0]);
} elseif (isset($ldapEntry['samaccountname'])) {
$this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
}
//homePath
if (str_starts_with($this->connection->homeFolderNamingRule, 'attr:')) {
$attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
if (isset($ldapEntry[$attr])) {
$this->access->cacheUserHome(
$this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
}
}
//memberOf groups
$cacheKey = 'getMemberOf'.$this->getUsername();
$groups = false;
if (isset($ldapEntry['memberof'])) {
$groups = $ldapEntry['memberof'];
}
$this->connection->writeToCache($cacheKey, $groups);
//external storage var
$attr = strtolower($this->connection->ldapExtStorageHomeAttribute);
if (isset($ldapEntry[$attr])) {
$this->updateExtStorageHome($ldapEntry[$attr][0]);
}
unset($attr);
// check for cached profile data
$username = $this->getUsername(); // buffer variable, to save resource
$cacheKey = 'getUserProfile-'.$username;
$profileCached = $this->connection->getFromCache($cacheKey);
// honoring profile disabled in config.php and check if user profile was refreshed
if ($this->config->getSystemValueBool('profile.enabled', true) &&
($profileCached === null) && // no cache or TTL not expired
!$this->wasRefreshed('profile')) {
// check current data
$profileValues = array();
//User Profile Field - Phone number
$attr = strtolower($this->connection->ldapAttributePhone);
if (!empty($attr)) { // attribute configured
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_PHONE]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - website
$attr = strtolower($this->connection->ldapAttributeWebsite);
if (isset($ldapEntry[$attr])) {
$cutPosition = strpos($ldapEntry[$attr][0], " ");
if ($cutPosition) {
// drop appended label
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE]
= substr($ldapEntry[$attr][0], 0, $cutPosition);
} else {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE]
= $ldapEntry[$attr][0];
}
} elseif (!empty($attr)) { // configured, but not defined
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE] = "";
}
//User Profile Field - Address
$attr = strtolower($this->connection->ldapAttributeAddress);
if (isset($ldapEntry[$attr])) {
if (str_contains($ldapEntry[$attr][0], '$')) {
// basic format conversion from postalAddress syntax to commata delimited
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS]
= str_replace('$', ", ", $ldapEntry[$attr][0]);
} else {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS]
= $ldapEntry[$attr][0];
}
} elseif (!empty($attr)) { // configured, but not defined
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS] = "";
}
//User Profile Field - Twitter
$attr = strtolower($this->connection->ldapAttributeTwitter);
if (!empty($attr)) {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_TWITTER]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - fediverse
$attr = strtolower($this->connection->ldapAttributeFediverse);
if (!empty($attr)) {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_FEDIVERSE]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - organisation
$attr = strtolower($this->connection->ldapAttributeOrganisation);
if (!empty($attr)) {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ORGANISATION]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - role
$attr = strtolower($this->connection->ldapAttributeRole);
if (!empty($attr)) {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ROLE]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - headline
$attr = strtolower($this->connection->ldapAttributeHeadline);
if (!empty($attr)) {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_HEADLINE]
= $ldapEntry[$attr][0] ?? "";
}
//User Profile Field - biography
$attr = strtolower($this->connection->ldapAttributeBiography);
if (isset($ldapEntry[$attr])) {
if (str_contains($ldapEntry[$attr][0], '\r')) {
// convert line endings
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY]
= str_replace(array("\r\n","\r"), "\n", $ldapEntry[$attr][0]);
} else {
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY]
= $ldapEntry[$attr][0];
}
} elseif (!empty($attr)) { // configured, but not defined
$profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY] = "";
}
// check for changed data and cache just for TTL checking
$checksum = hash('sha256', json_encode($profileValues));
$this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space
, null); // use ldapCacheTTL from configuration
// Update user profile
if ($this->config->getUserValue($username, 'user_ldap', 'lastProfileChecksum', null) !== $checksum) {
$this->config->setUserValue($username, 'user_ldap', 'lastProfileChecksum', $checksum);
$this->updateProfile($profileValues);
$this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']);
} else {
$this->logger->debug("profile data from LDAP unchanged", ['app' => 'user_ldap', 'uid' => $username]);
}
unset($attr);
} elseif ($profileCached !== null) { // message delayed, to declutter log
$this->logger->debug("skipping profile check, while cached data exist", ['app' => 'user_ldap', 'uid' => $username]);
}
//Avatar
/** @var Connection $connection */
$connection = $this->access->getConnection();
$attributes = $connection->resolveRule('avatar');
foreach ($attributes as $attribute) {
if (isset($ldapEntry[$attribute])) {
$this->avatarImage = $ldapEntry[$attribute][0];
// the call to the method that saves the avatar in the file
// system must be postponed after the login. It is to ensure
// external mounts are mounted properly (e.g. with login
// credentials from the session).
\OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
break;
}
}
}
/**
* @brief returns the LDAP DN of the user
* @return string
*/
public function getDN() {
return $this->dn;
}
/**
* @brief returns the Nextcloud internal username of the user
* @return string
*/
public function getUsername() {
return $this->uid;
}
/**
* returns the home directory of the user if specified by LDAP settings
* @param ?string $valueFromLDAP
* @return false|string
* @throws \Exception
*/
public function getHomePath($valueFromLDAP = null) {
$path = (string)$valueFromLDAP;
$attr = null;
if (is_null($valueFromLDAP)
&& str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:')
&& $this->access->connection->homeFolderNamingRule !== 'attr:') {
$attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
$homedir = $this->access->readAttribute($this->access->username2dn($this->getUsername()), $attr);
if ($homedir && isset($homedir[0])) {
$path = $homedir[0];
}
}
if ($path !== '') {
//if attribute's value is an absolute path take this, otherwise append it to data dir
//check for / at the beginning or pattern c:\ resp. c:/
if ('/' !== $path[0]
&& !(3 < strlen($path) && ctype_alpha($path[0])
&& $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2]))
) {
$path = $this->config->getSystemValue('datadirectory',
\OC::$SERVERROOT.'/data') . '/' . $path;
}
//we need it to store it in the DB as well in case a user gets
//deleted so we can clean up afterwards
$this->config->setUserValue(
$this->getUsername(), 'user_ldap', 'homePath', $path
);
return $path;
}
if (!is_null($attr)
&& $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true')
) {
// a naming rule attribute is defined, but it doesn't exist for that LDAP user
throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
}
//false will apply default behaviour as defined and done by OC_User
$this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
return false;
}
public function getMemberOfGroups() {
$cacheKey = 'getMemberOf'.$this->getUsername();
$memberOfGroups = $this->connection->getFromCache($cacheKey);
if (!is_null($memberOfGroups)) {
return $memberOfGroups;
}
$groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
$this->connection->writeToCache($cacheKey, $groupDNs);
return $groupDNs;
}
/**
* @brief reads the image from LDAP that shall be used as Avatar
* @return string data (provided by LDAP) | false
*/
public function getAvatarImage() {
if (!is_null($this->avatarImage)) {
return $this->avatarImage;
}
$this->avatarImage = false;
/** @var Connection $connection */
$connection = $this->access->getConnection();
$attributes = $connection->resolveRule('avatar');
foreach ($attributes as $attribute) {
$result = $this->access->readAttribute($this->dn, $attribute);
if ($result !== false && is_array($result) && isset($result[0])) {
$this->avatarImage = $result[0];
break;
}
}
return $this->avatarImage;
}
/**
* @brief marks the user as having logged in at least once
* @return null
*/
public function markLogin() {
$this->config->setUserValue(
$this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1');
}
/**
* Stores a key-value pair in relation to this user
*
* @param string $key
* @param string $value
*/
private function store($key, $value) {
$this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
}
/**
* Composes the display name and stores it in the database. The final
* display name is returned.
*
* @param string $displayName
* @param string $displayName2
* @return string the effective display name
*/
public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
$displayName2 = (string)$displayName2;
if ($displayName2 !== '') {
$displayName .= ' (' . $displayName2 . ')';
}
$oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null);
if ($oldName !== $displayName) {
$this->store('displayName', $displayName);
$user = $this->userManager->get($this->getUsername());
if (!empty($oldName) && $user instanceof \OC\User\User) {
// if it was empty, it would be a new record, not a change emitting the trigger could
// potentially cause a UniqueConstraintViolationException, depending on some factors.
$user->triggerChange('displayName', $displayName, $oldName);
}
}
return $displayName;
}
/**
* Stores the LDAP Username in the Database
* @param string $userName
*/
public function storeLDAPUserName($userName) {
$this->store('uid', $userName);
}
/**
* @brief checks whether an update method specified by feature was run
* already. If not, it will marked like this, because it is expected that
* the method will be run, when false is returned.
* @param string $feature email | quota | avatar | profile (can be extended)
* @return bool
*/
private function wasRefreshed($feature) {
if (isset($this->refreshedFeatures[$feature])) {
return true;
}
$this->refreshedFeatures[$feature] = 1;
return false;
}
/**
* fetches the email from LDAP and stores it as Nextcloud user value
* @param string $valueFromLDAP if known, to save an LDAP read request
* @return null
*/
public function updateEmail($valueFromLDAP = null) {
if ($this->wasRefreshed('email')) {
return;
}
$email = (string)$valueFromLDAP;
if (is_null($valueFromLDAP)) {
$emailAttribute = $this->connection->ldapEmailAttribute;
if ($emailAttribute !== '') {
$aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
if (is_array($aEmail) && (count($aEmail) > 0)) {
$email = (string)$aEmail[0];
}
}
}
if ($email !== '') {
$user = $this->userManager->get($this->uid);
if (!is_null($user)) {
$currentEmail = (string)$user->getSystemEMailAddress();
if ($currentEmail !== $email) {
$user->setEMailAddress($email);
}
}
}
}
/**
* Overall process goes as follow:
* 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
* 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
* 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
* 4. check if the target user exists and set the quota for the user.
*
* In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
* parameter can be passed with the value of the attribute. This value will be considered as the
* quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
* fetch all the user's attributes in one call and use the fetched values in this function.
* The expected value for that parameter is a string describing the quota for the user. Valid
* values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
* bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
*
* fetches the quota from LDAP and stores it as Nextcloud user value
* @param ?string $valueFromLDAP the quota attribute's value can be passed,
* to save the readAttribute request
* @return void
*/
public function updateQuota($valueFromLDAP = null) {
if ($this->wasRefreshed('quota')) {
return;
}
$quotaAttribute = $this->connection->ldapQuotaAttribute;
$defaultQuota = $this->connection->ldapQuotaDefault;
if ($quotaAttribute === '' && $defaultQuota === '') {
return;
}
$quota = false;
if (is_null($valueFromLDAP) && $quotaAttribute !== '') {
$aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
if ($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) {
$quota = $aQuota[0];
} elseif (is_array($aQuota) && isset($aQuota[0])) {
$this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']);
}
} elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) {
$quota = $valueFromLDAP;
} else {
$this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ['app' => 'user_ldap']);
}
if ($quota === false && $this->verifyQuotaValue($defaultQuota)) {
// quota not found using the LDAP attribute (or not parseable). Try the default quota
$quota = $defaultQuota;
} elseif ($quota === false) {
$this->logger->debug('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ['app' => 'user_ldap']);
return;
}
$targetUser = $this->userManager->get($this->uid);
if ($targetUser instanceof IUser) {
$targetUser->setQuota($quota);
} else {
$this->logger->info('trying to set a quota for user ' . $this->uid . ' but the user is missing', ['app' => 'user_ldap']);
}
}
private function verifyQuotaValue(string $quotaValue) {
return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
}
/**
* takes values from LDAP and stores it as Nextcloud user profile value
*
* @param array $profileValues associative array of property keys and values from LDAP
*/
private function updateProfile(array $profileValues): void {
// check if given array is empty
if (empty($profileValues)) {
return; // okay, nothing to do
}
// fetch/prepare user
$user = $this->userManager->get($this->uid);
if (is_null($user)) {
$this->logger->error('could not get user for uid='.$this->uid.'', ['app' => 'user_ldap']);
return;
}
// prepare AccountManager and Account
$accountManager = Server::get(IAccountManager::class);
$account = $accountManager->getAccount($user); // get Account
$defaultScopes = array_merge(AccountManager::DEFAULT_SCOPES,
$this->config->getSystemValue('account_manager.default_property_scope', []));
// loop through the properties and handle them
foreach ($profileValues as $property => $valueFromLDAP) {
// check and update profile properties
$value = (is_array($valueFromLDAP) ? $valueFromLDAP[0] : $valueFromLDAP); // take ONLY the first value, if multiple values specified
try {
$accountProperty = $account->getProperty($property);
$currentValue = $accountProperty->getValue();
$scope = ($accountProperty->getScope() ? $accountProperty->getScope()
: $defaultScopes[$property]);
} catch (PropertyDoesNotExistException $e) { // thrown at getProperty
$this->logger->error('property does not exist: '.$property
.' for uid='.$this->uid.'', ['app' => 'user_ldap', 'exception' => $e]);
$currentValue = '';
$scope = $defaultScopes[$property];
}
$verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what he put there
if ($currentValue !== $value) {
$account->setProperty($property, $value, $scope, $verified);
$this->logger->debug('update user profile: '.$property.'='.$value
.' for uid='.$this->uid.'', ['app' => 'user_ldap']);
}
}
try {
$accountManager->updateAccount($account); // may throw InvalidArgumentException
} catch (\InvalidArgumentException $e) {
$this->logger->error('invalid data from LDAP: for uid='.$this->uid.'', ['app' => 'user_ldap', 'func' => 'updateProfile'
, 'exception' => $e]);
}
}
/**
* called by a post_login hook to save the avatar picture
*
* @param array $params
*/
public function updateAvatarPostLogin($params) {
if (isset($params['uid']) && $params['uid'] === $this->getUsername()) {
$this->updateAvatar();
}
}
/**
* @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
* @return bool true when the avatar was set successfully or is up to date
*/
public function updateAvatar(bool $force = false): bool {
if (!$force && $this->wasRefreshed('avatar')) {
return false;
}
$avatarImage = $this->getAvatarImage();
if ($avatarImage === false) {
//not set, nothing left to do;
return false;
}
if (!$this->image->loadFromBase64(base64_encode($avatarImage))) {
return false;
}
// use the checksum before modifications
$checksum = md5($this->image->data());
if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '') && $this->avatarExists()) {
return true;
}
$isSet = $this->setOwnCloudAvatar();
if ($isSet) {
// save checksum only after successful setting
$this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum);
}
return $isSet;
}
private function avatarExists(): bool {
try {
$currentAvatar = $this->avatarManager->getAvatar($this->uid);
return $currentAvatar->exists() && $currentAvatar->isCustomAvatar();
} catch (\Exception $e) {
return false;
}
}
/**
* @brief sets an image as Nextcloud avatar
* @return bool
*/
private function setOwnCloudAvatar() {
if (!$this->image->valid()) {
$this->logger->error('avatar image data from LDAP invalid for '.$this->dn, ['app' => 'user_ldap']);
return false;
}
//make sure it is a square and not bigger than 512x512
$size = min([$this->image->width(), $this->image->height(), 512]);
if (!$this->image->centerCrop($size)) {
$this->logger->error('croping image for avatar failed for '.$this->dn, ['app' => 'user_ldap']);
return false;
}
if (!$this->fs->isLoaded()) {
$this->fs->setup($this->uid);
}
try {
$avatar = $this->avatarManager->getAvatar($this->uid);
$avatar->set($this->image);
return true;
} catch (\Exception $e) {
$this->logger->info('Could not set avatar for ' . $this->dn, ['exception' => $e]);
}
return false;
}
/**
* @throws AttributeNotSet
* @throws \OC\ServerNotAvailableException
* @throws \OCP\PreConditionNotMetException
*/
public function getExtStorageHome():string {
$value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', '');
if ($value !== '') {
return $value;
}
$value = $this->updateExtStorageHome();
if ($value !== '') {
return $value;
}
throw new AttributeNotSet(sprintf(
'external home storage attribute yield no value for %s', $this->getUsername()
));
}
/**
* @throws \OCP\PreConditionNotMetException
* @throws \OC\ServerNotAvailableException
*/
public function updateExtStorageHome(string $valueFromLDAP = null):string {
if ($valueFromLDAP === null) {
$extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute);
} else {
$extHomeValues = [$valueFromLDAP];
}
if ($extHomeValues && isset($extHomeValues[0])) {
$extHome = $extHomeValues[0];
$this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome);
return $extHome;
} else {
$this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome');
return '';
}
}
/**
* called by a post_login hook to handle password expiry
*
* @param array $params
*/
public function handlePasswordExpiry($params) {
$ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
return;//password expiry handling disabled
}
$uid = $params['uid'];
if (isset($uid) && $uid === $this->getUsername()) {
//retrieve relevant user attributes
$result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
if (array_key_exists('pwdpolicysubentry', $result[0])) {
$pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) {
$ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
}
}
$pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : [];
$pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : [];
$pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : [];
//retrieve relevant password policy attributes
$cacheKey = 'ppolicyAttributes' . $ppolicyDN;
$result = $this->connection->getFromCache($cacheKey);
if (is_null($result)) {
$result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
$this->connection->writeToCache($cacheKey, $result);
}
$pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : [];
$pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : [];
$pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : [];
//handle grace login
if (!empty($pwdGraceUseTime)) { //was this a grace login?
if (!empty($pwdGraceAuthNLimit)
&& count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available?
$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid]));
} else { //no more grace login available
header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid]));
}
exit();
}
//handle pwdReset attribute
if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change his password
$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid]));
exit();
}
//handle password expiry warning
if (!empty($pwdChangedTime)) {
if (!empty($pwdMaxAge)
&& !empty($pwdExpireWarning)) {
$pwdMaxAgeInt = (int)$pwdMaxAge[0];
$pwdExpireWarningInt = (int)$pwdExpireWarning[0];
if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) {
$pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
$pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S'));
$currentDateTime = new \DateTime();
$secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
if ($secondsToExpiry <= $pwdExpireWarningInt) {
//remove last password expiry warning if any
$notification = $this->notificationManager->createNotification();
$notification->setApp('user_ldap')
->setUser($uid)
->setObject('pwd_exp_warn', $uid)
;
$this->notificationManager->markProcessed($notification);
//create new password expiry warning
$notification = $this->notificationManager->createNotification();
$notification->setApp('user_ldap')
->setUser($uid)
->setDateTime($currentDateTime)
->setObject('pwd_exp_warn', $uid)
->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)])
;
$this->notificationManager->notify($notification);
}
}
}
}
}
}
}
@@ -0,0 +1,223 @@
<?php
/**
* @copyright Copyright (c) 2017 EITA Cooperative (eita.org.br)
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Filis Futsarov <filisko@users.noreply.github.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\User_LDAP;
use OC\User\Backend;
class UserPluginManager {
private int $respondToActions = 0;
private array $which = [
Backend::CREATE_USER => null,
Backend::SET_PASSWORD => null,
Backend::GET_HOME => null,
Backend::GET_DISPLAYNAME => null,
Backend::SET_DISPLAYNAME => null,
Backend::PROVIDE_AVATAR => null,
Backend::COUNT_USERS => null,
'deleteUser' => null
];
private bool $suppressDeletion = false;
/**
* @return int All implemented actions, except for 'deleteUser'
*/
public function getImplementedActions() {
return $this->respondToActions;
}
/**
* Registers a user plugin that may implement some actions, overriding User_LDAP's user actions.
*
* @param ILDAPUserPlugin $plugin
*/
public function register(ILDAPUserPlugin $plugin) {
$respondToActions = $plugin->respondToActions();
$this->respondToActions |= $respondToActions;
foreach ($this->which as $action => $v) {
if (is_int($action) && (bool)($respondToActions & $action)) {
$this->which[$action] = $plugin;
\OC::$server->getLogger()->debug("Registered action ".$action." to plugin ".get_class($plugin), ['app' => 'user_ldap']);
}
}
if (method_exists($plugin, 'deleteUser')) {
$this->which['deleteUser'] = $plugin;
\OC::$server->getLogger()->debug("Registered action deleteUser to plugin ".get_class($plugin), ['app' => 'user_ldap']);
}
}
/**
* Signal if there is a registered plugin that implements some given actions
* @param int $actions Actions defined in \OC\User\Backend, like Backend::CREATE_USER
* @return bool
*/
public function implementsActions($actions) {
return ($actions & $this->respondToActions) == $actions;
}
/**
* Create a new user in LDAP Backend
*
* @param string $username The username of the user to create
* @param string $password The password of the new user
* @return string | false The user DN if user creation was successful.
* @throws \Exception
*/
public function createUser($username, $password) {
$plugin = $this->which[Backend::CREATE_USER];
if ($plugin) {
return $plugin->createUser($username, $password);
}
throw new \Exception('No plugin implements createUser in this LDAP Backend.');
}
/**
* Change the password of a user*
* @param string $uid The username
* @param string $password The new password
* @return bool
* @throws \Exception
*/
public function setPassword($uid, $password) {
$plugin = $this->which[Backend::SET_PASSWORD];
if ($plugin) {
return $plugin->setPassword($uid, $password);
}
throw new \Exception('No plugin implements setPassword in this LDAP Backend.');
}
/**
* checks whether the user is allowed to change his avatar in Nextcloud
* @param string $uid the Nextcloud user name
* @return boolean either the user can or cannot
* @throws \Exception
*/
public function canChangeAvatar($uid) {
$plugin = $this->which[Backend::PROVIDE_AVATAR];
if ($plugin) {
return $plugin->canChangeAvatar($uid);
}
throw new \Exception('No plugin implements canChangeAvatar in this LDAP Backend.');
}
/**
* Get the user's home directory
* @param string $uid the username
* @return boolean
* @throws \Exception
*/
public function getHome($uid) {
$plugin = $this->which[Backend::GET_HOME];
if ($plugin) {
return $plugin->getHome($uid);
}
throw new \Exception('No plugin implements getHome in this LDAP Backend.');
}
/**
* Get display name of the user
* @param string $uid user ID of the user
* @return string display name
* @throws \Exception
*/
public function getDisplayName($uid) {
$plugin = $this->which[Backend::GET_DISPLAYNAME];
if ($plugin) {
return $plugin->getDisplayName($uid);
}
throw new \Exception('No plugin implements getDisplayName in this LDAP Backend.');
}
/**
* Set display name of the user
* @param string $uid user ID of the user
* @param string $displayName new user's display name
* @return string display name
* @throws \Exception
*/
public function setDisplayName($uid, $displayName) {
$plugin = $this->which[Backend::SET_DISPLAYNAME];
if ($plugin) {
return $plugin->setDisplayName($uid, $displayName);
}
throw new \Exception('No plugin implements setDisplayName in this LDAP Backend.');
}
/**
* Count the number of users
* @return int|false
* @throws \Exception
*/
public function countUsers() {
$plugin = $this->which[Backend::COUNT_USERS];
if ($plugin) {
return $plugin->countUsers();
}
throw new \Exception('No plugin implements countUsers in this LDAP Backend.');
}
/**
* @return bool
*/
public function canDeleteUser() {
return !$this->suppressDeletion && $this->which['deleteUser'] !== null;
}
/**
* @param $uid
* @return bool
* @throws \Exception
*/
public function deleteUser($uid) {
$plugin = $this->which['deleteUser'];
if ($plugin) {
if ($this->suppressDeletion) {
return false;
}
return $plugin->deleteUser($uid);
}
throw new \Exception('No plugin implements deleteUser in this LDAP Backend.');
}
/**
* @param bool $value
* @return bool – the value before the change
*/
public function setSuppressDeletion(bool $value): bool {
$old = $this->suppressDeletion;
$this->suppressDeletion = $value;
return $old;
}
}
@@ -0,0 +1,688 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Dominik Schmidt <dev@dominik-schmidt.de>
* @author felixboehm <felix@webhippie.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roger Szabo <roger.szabo@web.de>
* @author root <root@localhost.localdomain>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tom Needham <tom@owncloud.com>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OC\ServerNotAvailableException;
use OC\User\Backend;
use OC\User\NoUserException;
use OCA\User_LDAP\Exceptions\NotOnLDAP;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User\OfflineUser;
use OCA\User_LDAP\User\User;
use OCP\IConfig;
use OCP\IUserBackend;
use OCP\IUserSession;
use OCP\Notification\IManager as INotificationManager;
use OCP\User\Backend\ICountMappedUsersBackend;
use OCP\User\Backend\ICountUsersBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;
class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ICountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend {
protected IConfig $ocConfig;
protected INotificationManager $notificationManager;
protected UserPluginManager $userPluginManager;
protected LoggerInterface $logger;
protected DeletedUsersIndex $deletedUsersIndex;
public function __construct(
Access $access,
IConfig $ocConfig,
INotificationManager $notificationManager,
IUserSession $userSession,
UserPluginManager $userPluginManager,
LoggerInterface $logger,
DeletedUsersIndex $deletedUsersIndex,
) {
parent::__construct($access);
$this->ocConfig = $ocConfig;
$this->notificationManager = $notificationManager;
$this->userPluginManager = $userPluginManager;
$this->logger = $logger;
$this->deletedUsersIndex = $deletedUsersIndex;
}
/**
* checks whether the user is allowed to change his avatar in Nextcloud
*
* @param string $uid the Nextcloud user name
* @return boolean either the user can or cannot
* @throws \Exception
*/
public function canChangeAvatar($uid) {
if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
return $this->userPluginManager->canChangeAvatar($uid);
}
if (!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
return true;
}
$user = $this->access->userManager->get($uid);
if (!$user instanceof User) {
return false;
}
$imageData = $user->getAvatarImage();
if ($imageData === false) {
return true;
}
return !$user->updateAvatar(true);
}
/**
* Return the username for the given login name, if available
*
* @param string $loginName
* @return string|false
* @throws \Exception
*/
public function loginName2UserName($loginName) {
$cacheKey = 'loginName2UserName-' . $loginName;
$username = $this->access->connection->getFromCache($cacheKey);
if ($username !== null) {
return $username;
}
try {
$ldapRecord = $this->getLDAPUserByLoginName($loginName);
$user = $this->access->userManager->get($ldapRecord['dn'][0]);
if ($user === null || $user instanceof OfflineUser) {
// this path is not really possible, however get() is documented
// to return User, OfflineUser or null so we are very defensive here.
$this->access->connection->writeToCache($cacheKey, false);
return false;
}
$username = $user->getUsername();
$this->access->connection->writeToCache($cacheKey, $username);
return $username;
} catch (NotOnLDAP $e) {
$this->access->connection->writeToCache($cacheKey, false);
return false;
}
}
/**
* returns the username for the given LDAP DN, if available
*
* @param string $dn
* @return string|false with the username
*/
public function dn2UserName($dn) {
return $this->access->dn2username($dn);
}
/**
* returns an LDAP record based on a given login name
*
* @param string $loginName
* @return array
* @throws NotOnLDAP
*/
public function getLDAPUserByLoginName($loginName) {
//find out dn of the user name
$attrs = $this->access->userManager->getAttributes();
$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
if (count($users) < 1) {
throw new NotOnLDAP('No user available for the given login name on ' .
$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
}
return $users[0];
}
/**
* Check if the password is correct without logging in the user
*
* @param string $uid The username
* @param string $password The password
* @return false|string
*/
public function checkPassword($uid, $password) {
try {
$ldapRecord = $this->getLDAPUserByLoginName($uid);
} catch (NotOnLDAP $e) {
$this->logger->debug(
$e->getMessage(),
['app' => 'user_ldap', 'exception' => $e]
);
return false;
}
$dn = $ldapRecord['dn'][0];
$user = $this->access->userManager->get($dn);
if (!$user instanceof User) {
$this->logger->warning(
'LDAP Login: Could not get user object for DN ' . $dn .
'. Maybe the LDAP entry has no set display name attribute?',
['app' => 'user_ldap']
);
return false;
}
if ($user->getUsername() !== false) {
//are the credentials OK?
if (!$this->access->areCredentialsValid($dn, $password)) {
return false;
}
$this->access->cacheUserExists($user->getUsername());
$user->processAttributes($ldapRecord);
$user->markLogin();
return $user->getUsername();
}
return false;
}
/**
* Set password
* @param string $uid The username
* @param string $password The new password
* @return bool
*/
public function setPassword($uid, $password) {
if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
return $this->userPluginManager->setPassword($uid, $password);
}
$user = $this->access->userManager->get($uid);
if (!$user instanceof User) {
throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
'. Maybe the LDAP entry has no set display name attribute?');
}
if ($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
$ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
$turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
//remove last password expiry warning if any
$notification = $this->notificationManager->createNotification();
$notification->setApp('user_ldap')
->setUser($uid)
->setObject('pwd_exp_warn', $uid)
;
$this->notificationManager->markProcessed($notification);
}
return true;
}
return false;
}
/**
* Get a list of all users
*
* @param string $search
* @param integer $limit
* @param integer $offset
* @return string[] an array of all uids
*/
public function getUsers($search = '', $limit = 10, $offset = 0) {
$search = $this->access->escapeFilterPart($search, true);
$cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
//check if users are cached, if so return
$ldap_users = $this->access->connection->getFromCache($cachekey);
if (!is_null($ldap_users)) {
return $ldap_users;
}
// if we'd pass -1 to LDAP search, we'd end up in a Protocol
// error. With a limit of 0, we get 0 results. So we pass null.
if ($limit <= 0) {
$limit = null;
}
$filter = $this->access->combineFilterWithAnd([
$this->access->connection->ldapUserFilter,
$this->access->connection->ldapUserDisplayName . '=*',
$this->access->getFilterPartForUserSearch($search)
]);
$this->logger->debug(
'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
['app' => 'user_ldap']
);
//do the search and translate results to Nextcloud names
$ldap_users = $this->access->fetchListOfUsers(
$filter,
$this->access->userManager->getAttributes(true),
$limit, $offset);
$ldap_users = $this->access->nextcloudUserNames($ldap_users);
$this->logger->debug(
'getUsers: '.count($ldap_users). ' Users found',
['app' => 'user_ldap']
);
$this->access->connection->writeToCache($cachekey, $ldap_users);
return $ldap_users;
}
/**
* checks whether a user is still available on LDAP
*
* @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
* name or an instance of that user
* @throws \Exception
* @throws \OC\ServerNotAvailableException
*/
public function userExistsOnLDAP($user, bool $ignoreCache = false): bool {
if (is_string($user)) {
$user = $this->access->userManager->get($user);
}
if (is_null($user)) {
return false;
}
$uid = $user instanceof User ? $user->getUsername() : $user->getOCName();
$cacheKey = 'userExistsOnLDAP' . $uid;
if (!$ignoreCache) {
$userExists = $this->access->connection->getFromCache($cacheKey);
if (!is_null($userExists)) {
return (bool)$userExists;
}
}
$dn = $user->getDN();
//check if user really still exists by reading its entry
if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
try {
$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
if (!$uuid) {
$this->access->connection->writeToCache($cacheKey, false);
return false;
}
$newDn = $this->access->getUserDnByUuid($uuid);
//check if renamed user is still valid by reapplying the ldap filter
if ($newDn === $dn || !is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
$this->access->connection->writeToCache($cacheKey, false);
return false;
}
$this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
} catch (ServerNotAvailableException $e) {
throw $e;
} catch (\Exception $e) {
$this->access->connection->writeToCache($cacheKey, false);
return false;
}
}
if ($user instanceof OfflineUser) {
$user->unmark();
}
$this->access->connection->writeToCache($cacheKey, true);
return true;
}
/**
* check if a user exists
* @param string $uid the username
* @return boolean
* @throws \Exception when connection could not be established
*/
public function userExists($uid) {
$userExists = $this->access->connection->getFromCache('userExists'.$uid);
if (!is_null($userExists)) {
return (bool)$userExists;
}
//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
$user = $this->access->userManager->get($uid);
if (is_null($user)) {
$this->logger->debug(
'No DN found for '.$uid.' on '.$this->access->connection->ldapHost,
['app' => 'user_ldap']
);
$this->access->connection->writeToCache('userExists'.$uid, false);
return false;
}
$this->access->connection->writeToCache('userExists'.$uid, true);
return true;
}
/**
* returns whether a user was deleted in LDAP
*
* @param string $uid The username of the user to delete
* @return bool
*/
public function deleteUser($uid) {
if ($this->userPluginManager->canDeleteUser()) {
$status = $this->userPluginManager->deleteUser($uid);
if ($status === false) {
return false;
}
}
$marked = $this->deletedUsersIndex->isUserMarked($uid);
if (!$marked) {
try {
$user = $this->access->userManager->get($uid);
if (($user instanceof User) && !$this->userExistsOnLDAP($uid, true)) {
$user->markUser();
$marked = true;
}
} catch (\Exception $e) {
$this->logger->debug(
$e->getMessage(),
['app' => 'user_ldap', 'exception' => $e]
);
}
if (!$marked) {
$this->logger->notice(
'User '.$uid . ' is not marked as deleted, not cleaning up.',
['app' => 'user_ldap']
);
return false;
}
}
$this->logger->info('Cleaning up after user ' . $uid,
['app' => 'user_ldap']);
$this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
$this->access->userManager->invalidate($uid);
$this->access->connection->clearCache();
return true;
}
/**
* get the user's home directory
*
* @param string $uid the username
* @return bool|string
* @throws NoUserException
* @throws \Exception
*/
public function getHome($uid) {
// user Exists check required as it is not done in user proxy!
if (!$this->userExists($uid)) {
return false;
}
if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
return $this->userPluginManager->getHome($uid);
}
$cacheKey = 'getHome'.$uid;
$path = $this->access->connection->getFromCache($cacheKey);
if (!is_null($path)) {
return $path;
}
// early return path if it is a deleted user
$user = $this->access->userManager->get($uid);
if ($user instanceof User || $user instanceof OfflineUser) {
$path = $user->getHomePath() ?: false;
} else {
throw new NoUserException($uid . ' is not a valid user anymore');
}
$this->access->cacheUserHome($uid, $path);
return $path;
}
/**
* get display name of the user
* @param string $uid user ID of the user
* @return string|false display name
*/
public function getDisplayName($uid) {
if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
return $this->userPluginManager->getDisplayName($uid);
}
if (!$this->userExists($uid)) {
return false;
}
$cacheKey = 'getDisplayName'.$uid;
if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
return $displayName;
}
//Check whether the display name is configured to have a 2nd feature
$additionalAttribute = $this->access->connection->ldapUserDisplayName2;
$displayName2 = '';
if ($additionalAttribute !== '') {
$displayName2 = $this->access->readAttribute(
$this->access->username2dn($uid),
$additionalAttribute);
}
$displayName = $this->access->readAttribute(
$this->access->username2dn($uid),
$this->access->connection->ldapUserDisplayName);
if ($displayName && (count($displayName) > 0)) {
$displayName = $displayName[0];
if (is_array($displayName2)) {
$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
}
$user = $this->access->userManager->get($uid);
if ($user instanceof User) {
$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
$this->access->connection->writeToCache($cacheKey, $displayName);
}
if ($user instanceof OfflineUser) {
/** @var OfflineUser $user*/
$displayName = $user->getDisplayName();
}
return $displayName;
}
return null;
}
/**
* set display name of the user
* @param string $uid user ID of the user
* @param string $displayName new display name of the user
* @return string|false display name
*/
public function setDisplayName($uid, $displayName) {
if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
$this->userPluginManager->setDisplayName($uid, $displayName);
$this->access->cacheUserDisplayName($uid, $displayName);
return $displayName;
}
return false;
}
/**
* Get a list of all display names
*
* @param string $search
* @param int|null $limit
* @param int|null $offset
* @return array an array of all displayNames (value) and the corresponding uids (key)
*/
public function getDisplayNames($search = '', $limit = null, $offset = null) {
$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
if (!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
return $displayNames;
}
$displayNames = [];
$users = $this->getUsers($search, $limit, $offset);
foreach ($users as $user) {
$displayNames[$user] = $this->getDisplayName($user);
}
$this->access->connection->writeToCache($cacheKey, $displayNames);
return $displayNames;
}
/**
* Check if backend implements actions
* @param int $actions bitwise-or'ed actions
* @return boolean
*
* Returns the supported actions as int to be
* compared with \OC\User\Backend::CREATE_USER etc.
*/
public function implementsActions($actions) {
return (bool)((Backend::CHECK_PASSWORD
| Backend::GET_HOME
| Backend::GET_DISPLAYNAME
| (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0)
| Backend::COUNT_USERS
| (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
| $this->userPluginManager->getImplementedActions())
& $actions);
}
/**
* @return bool
*/
public function hasUserListings() {
return true;
}
/**
* counts the users in LDAP
*
* @return int|false
*/
public function countUsers() {
if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
return $this->userPluginManager->countUsers();
}
$filter = $this->access->getFilterForUserCount();
$cacheKey = 'countUsers-'.$filter;
if (!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
return $entries;
}
$entries = $this->access->countUsers($filter);
$this->access->connection->writeToCache($cacheKey, $entries);
return $entries;
}
public function countMappedUsers(): int {
return $this->access->getUserMapper()->count();
}
/**
* Backend name to be shown in user management
* @return string the name of the backend to be shown
*/
public function getBackendName() {
return 'LDAP';
}
/**
* Return access for LDAP interaction.
* @param string $uid
* @return Access instance of Access for LDAP interaction
*/
public function getLDAPAccess($uid) {
return $this->access;
}
/**
* Return LDAP connection resource from a cloned connection.
* The cloned connection needs to be closed manually.
* of the current access.
* @param string $uid
* @return resource|\LDAP\Connection The LDAP connection
*/
public function getNewLDAPConnection($uid) {
$connection = clone $this->access->getConnection();
return $connection->getConnectionResource();
}
/**
* create new user
* @param string $username username of the new user
* @param string $password password of the new user
* @throws \UnexpectedValueException
* @return bool
*/
public function createUser($username, $password) {
if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
if ($dn = $this->userPluginManager->createUser($username, $password)) {
if (is_string($dn)) {
// the NC user creation work flow requires a know user id up front
$uuid = $this->access->getUUID($dn, true);
if (is_string($uuid)) {
$this->access->mapAndAnnounceIfApplicable(
$this->access->getUserMapper(),
$dn,
$username,
$uuid,
true
);
$this->access->cacheUserExists($username);
} else {
$this->logger->warning(
'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
[
'app' => 'user_ldap',
'userid' => $username,
]
);
}
} else {
throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean.");
}
}
return (bool) $dn;
}
return false;
}
public function isUserEnabled(string $uid, callable $queryDatabaseValue): bool {
if ($this->deletedUsersIndex->isUserMarked($uid) && ((int)$this->access->connection->markRemnantsAsDisabled === 1)) {
return false;
} else {
return $queryDatabaseValue();
}
}
public function setUserEnabled(string $uid, bool $enabled, callable $queryDatabaseValue, callable $setDatabaseValue): bool {
$setDatabaseValue($enabled);
return $enabled;
}
public function getDisabledUserList(?int $limit = null, int $offset = 0): array {
throw new \Exception('This is implemented directly in User_Proxy');
}
}
@@ -0,0 +1,476 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roger Szabo <roger.szabo@web.de>
* @author root <root@localhost.localdomain>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User\OfflineUser;
use OCA\User_LDAP\User\User;
use OCP\IConfig;
use OCP\IUserBackend;
use OCP\IUserSession;
use OCP\Notification\IManager as INotificationManager;
use OCP\User\Backend\ICountMappedUsersBackend;
use OCP\User\Backend\ICountUsersBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;
class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP, ICountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend {
/** @var User_LDAP[] */
private array $backends = [];
private ?User_LDAP $refBackend = null;
private bool $isSetUp = false;
private Helper $helper;
private IConfig $ocConfig;
private INotificationManager $notificationManager;
private IUserSession $userSession;
private UserPluginManager $userPluginManager;
private LoggerInterface $logger;
private DeletedUsersIndex $deletedUsersIndex;
public function __construct(
Helper $helper,
ILDAPWrapper $ldap,
AccessFactory $accessFactory,
IConfig $ocConfig,
INotificationManager $notificationManager,
IUserSession $userSession,
UserPluginManager $userPluginManager,
LoggerInterface $logger,
DeletedUsersIndex $deletedUsersIndex,
) {
parent::__construct($ldap, $accessFactory);
$this->helper = $helper;
$this->ocConfig = $ocConfig;
$this->notificationManager = $notificationManager;
$this->userSession = $userSession;
$this->userPluginManager = $userPluginManager;
$this->logger = $logger;
$this->deletedUsersIndex = $deletedUsersIndex;
}
protected function setup(): void {
if ($this->isSetUp) {
return;
}
$serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true);
foreach ($serverConfigPrefixes as $configPrefix) {
$this->backends[$configPrefix] = new User_LDAP(
$this->getAccess($configPrefix),
$this->ocConfig,
$this->notificationManager,
$this->userSession,
$this->userPluginManager,
$this->logger,
$this->deletedUsersIndex,
);
if (is_null($this->refBackend)) {
$this->refBackend = &$this->backends[$configPrefix];
}
}
$this->isSetUp = true;
}
/**
* Tries the backends one after the other until a positive result is returned from the specified method
*
* @param string $id the uid connected to the request
* @param string $method the method of the user backend that shall be called
* @param array $parameters an array of parameters to be passed
* @return mixed the result of the method or false
*/
protected function walkBackends($id, $method, $parameters) {
$this->setup();
$uid = $id;
$cacheKey = $this->getUserCacheKey($uid);
foreach ($this->backends as $configPrefix => $backend) {
$instance = $backend;
if (!method_exists($instance, $method)
&& method_exists($this->getAccess($configPrefix), $method)) {
$instance = $this->getAccess($configPrefix);
}
if ($result = call_user_func_array([$instance, $method], $parameters)) {
if (!$this->isSingleBackend()) {
$this->writeToCache($cacheKey, $configPrefix);
}
return $result;
}
}
return false;
}
/**
* Asks the backend connected to the server that supposely takes care of the uid from the request.
*
* @param string $id the uid connected to the request
* @param string $method the method of the user backend that shall be called
* @param array $parameters an array of parameters to be passed
* @param mixed $passOnWhen the result matches this variable
* @return mixed the result of the method or false
*/
protected function callOnLastSeenOn($id, $method, $parameters, $passOnWhen) {
$this->setup();
$uid = $id;
$cacheKey = $this->getUserCacheKey($uid);
$prefix = $this->getFromCache($cacheKey);
//in case the uid has been found in the past, try this stored connection first
if (!is_null($prefix)) {
if (isset($this->backends[$prefix])) {
$instance = $this->backends[$prefix];
if (!method_exists($instance, $method)
&& method_exists($this->getAccess($prefix), $method)) {
$instance = $this->getAccess($prefix);
}
$result = call_user_func_array([$instance, $method], $parameters);
if ($result === $passOnWhen) {
//not found here, reset cache to null if user vanished
//because sometimes methods return false with a reason
$userExists = call_user_func_array(
[$this->backends[$prefix], 'userExistsOnLDAP'],
[$uid]
);
if (!$userExists) {
$this->writeToCache($cacheKey, null);
}
}
return $result;
}
}
return false;
}
protected function activeBackends(): int {
$this->setup();
return count($this->backends);
}
/**
* Check if backend implements actions
*
* @param int $actions bitwise-or'ed actions
* @return boolean
*
* Returns the supported actions as int to be
* compared with \OC\User\Backend::CREATE_USER etc.
*/
public function implementsActions($actions) {
$this->setup();
//it's the same across all our user backends obviously
return $this->refBackend->implementsActions($actions);
}
/**
* Backend name to be shown in user management
*
* @return string the name of the backend to be shown
*/
public function getBackendName() {
$this->setup();
return $this->refBackend->getBackendName();
}
/**
* Get a list of all users
*
* @param string $search
* @param null|int $limit
* @param null|int $offset
* @return string[] an array of all uids
*/
public function getUsers($search = '', $limit = 10, $offset = 0) {
$this->setup();
//we do it just as the /OC_User implementation: do not play around with limit and offset but ask all backends
$users = [];
foreach ($this->backends as $backend) {
$backendUsers = $backend->getUsers($search, $limit, $offset);
if (is_array($backendUsers)) {
$users = array_merge($users, $backendUsers);
}
}
return $users;
}
/**
* check if a user exists
*
* @param string $uid the username
* @return boolean
*/
public function userExists($uid) {
$existsOnLDAP = false;
$existsLocally = $this->handleRequest($uid, 'userExists', [$uid]);
if ($existsLocally) {
$existsOnLDAP = $this->userExistsOnLDAP($uid);
}
if ($existsLocally && !$existsOnLDAP) {
try {
$user = $this->getLDAPAccess($uid)->userManager->get($uid);
if ($user instanceof User) {
$user->markUser();
}
} catch (\Exception $e) {
// ignore
}
}
return $existsLocally;
}
/**
* check if a user exists on LDAP
*
* @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
* name or an instance of that user
*/
public function userExistsOnLDAP($user, bool $ignoreCache = false): bool {
$id = ($user instanceof User) ? $user->getUsername() : $user;
return $this->handleRequest($id, 'userExistsOnLDAP', [$user, $ignoreCache]);
}
/**
* Check if the password is correct
*
* @param string $uid The username
* @param string $password The password
* @return bool
*
* Check if the password is correct without logging in the user
*/
public function checkPassword($uid, $password) {
return $this->handleRequest($uid, 'checkPassword', [$uid, $password]);
}
/**
* returns the username for the given login name, if available
*
* @param string $loginName
* @return string|false
*/
public function loginName2UserName($loginName) {
$id = 'LOGINNAME,' . $loginName;
return $this->handleRequest($id, 'loginName2UserName', [$loginName]);
}
/**
* returns the username for the given LDAP DN, if available
*
* @param string $dn
* @return string|false with the username
*/
public function dn2UserName($dn) {
$id = 'DN,' . $dn;
return $this->handleRequest($id, 'dn2UserName', [$dn]);
}
/**
* get the user's home directory
*
* @param string $uid the username
* @return boolean
*/
public function getHome($uid) {
return $this->handleRequest($uid, 'getHome', [$uid]);
}
/**
* get display name of the user
*
* @param string $uid user ID of the user
* @return string display name
*/
public function getDisplayName($uid) {
return $this->handleRequest($uid, 'getDisplayName', [$uid]);
}
/**
* set display name of the user
*
* @param string $uid user ID of the user
* @param string $displayName new display name
* @return string display name
*/
public function setDisplayName($uid, $displayName) {
return $this->handleRequest($uid, 'setDisplayName', [$uid, $displayName]);
}
/**
* checks whether the user is allowed to change his avatar in Nextcloud
*
* @param string $uid the Nextcloud user name
* @return boolean either the user can or cannot
*/
public function canChangeAvatar($uid) {
return $this->handleRequest($uid, 'canChangeAvatar', [$uid], true);
}
/**
* Get a list of all display names and user ids.
*
* @param string $search
* @param int|null $limit
* @param int|null $offset
* @return array an array of all displayNames (value) and the corresponding uids (key)
*/
public function getDisplayNames($search = '', $limit = null, $offset = null) {
$this->setup();
//we do it just as the /OC_User implementation: do not play around with limit and offset but ask all backends
$users = [];
foreach ($this->backends as $backend) {
$backendUsers = $backend->getDisplayNames($search, $limit, $offset);
if (is_array($backendUsers)) {
$users = $users + $backendUsers;
}
}
return $users;
}
/**
* delete a user
*
* @param string $uid The username of the user to delete
* @return bool
*
* Deletes a user
*/
public function deleteUser($uid) {
return $this->handleRequest($uid, 'deleteUser', [$uid]);
}
/**
* Set password
*
* @param string $uid The username
* @param string $password The new password
* @return bool
*
*/
public function setPassword($uid, $password) {
return $this->handleRequest($uid, 'setPassword', [$uid, $password]);
}
/**
* @return bool
*/
public function hasUserListings() {
$this->setup();
return $this->refBackend->hasUserListings();
}
/**
* Count the number of users
*
* @return int|false
*/
public function countUsers() {
$this->setup();
$users = false;
foreach ($this->backends as $backend) {
$backendUsers = $backend->countUsers();
if ($backendUsers !== false) {
$users = (int)$users + $backendUsers;
}
}
return $users;
}
/**
* Count the number of mapped users
*/
public function countMappedUsers(): int {
$this->setup();
$users = 0;
foreach ($this->backends as $backend) {
$users += $backend->countMappedUsers();
}
return $users;
}
/**
* Return access for LDAP interaction.
*
* @param string $uid
* @return Access instance of Access for LDAP interaction
*/
public function getLDAPAccess($uid) {
return $this->handleRequest($uid, 'getLDAPAccess', [$uid]);
}
/**
* Return a new LDAP connection for the specified user.
* The connection needs to be closed manually.
*
* @param string $uid
* @return resource|\LDAP\Connection The LDAP connection
*/
public function getNewLDAPConnection($uid) {
return $this->handleRequest($uid, 'getNewLDAPConnection', [$uid]);
}
/**
* Creates a new user in LDAP
*
* @param $username
* @param $password
* @return bool
*/
public function createUser($username, $password) {
return $this->handleRequest($username, 'createUser', [$username, $password]);
}
public function isUserEnabled(string $uid, callable $queryDatabaseValue): bool {
return $this->handleRequest($uid, 'isUserEnabled', [$uid, $queryDatabaseValue]);
}
public function setUserEnabled(string $uid, bool $enabled, callable $queryDatabaseValue, callable $setDatabaseValue): bool {
return $this->handleRequest($uid, 'setUserEnabled', [$uid, $enabled, $queryDatabaseValue, $setDatabaseValue]);
}
public function getDisabledUserList(?int $limit = null, int $offset = 0): array {
return array_map(
fn (OfflineUser $user) => $user->getOCName(),
array_slice(
$this->deletedUsersIndex->getUsers(),
$offset,
$limit
)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP;
class WizardResult {
protected $changes = [];
protected $options = [];
protected $markedChange = false;
/**
* @param string $key
* @param mixed $value
*/
public function addChange($key, $value) {
$this->changes[$key] = $value;
}
public function markChange() {
$this->markedChange = true;
}
/**
* @param string $key
* @param array|string $values
*/
public function addOptions($key, $values) {
if (!is_array($values)) {
$values = [$values];
}
$this->options[$key] = $values;
}
/**
* @return bool
*/
public function hasChanges() {
return (count($this->changes) > 0 || $this->markedChange);
}
/**
* @return array
*/
public function getResultArray() {
$result = [];
$result['changes'] = $this->changes;
if (count($this->options) > 0) {
$result['options'] = $this->options;
}
return $result;
}
}