diff options
author | Arthur Schiwon <blizzz@owncloud.com> | 2014-08-21 17:59:13 +0200 |
---|---|---|
committer | Arthur Schiwon <blizzz@owncloud.com> | 2014-12-19 19:47:54 +0100 |
commit | 4fa39250e714b3ee5aa16a5f9ce8c77daa44311b (patch) | |
tree | 5c21fabd2942fdc462750625cd29e544c708ac05 | |
parent | 8164415b45386cb87e05e6e50cf4b8f3128b2e69 (diff) | |
download | nextcloud-server-4fa39250e714b3ee5aa16a5f9ce8c77daa44311b.tar.gz nextcloud-server-4fa39250e714b3ee5aa16a5f9ce8c77daa44311b.zip |
LDAP User Cleanup: Port from stable7 without further adjustements
LDAP User Cleanup
background job for user clean up
adjust user backend for clean up
register background job
remove dead code
dependency injection
make Helper non-static for proper testing
check whether it is OK to run clean up job. Do not forget to pass arguments.
use correct method to get the config from server
methods can be private, proper indirect testing is given
no automatic user deletion
make limit readable for test purposes
make method less complex
add first tests
let preferences accept limit and offset for getUsersForValue
DI via constructor does not work for background jobs
after detecting, now we have retrieving deleted users and their details
we need this method to be public for now
finalize export method, add missing getter
clean up namespaces and get rid of unnecessary files
helper is not static anymore
cleanup according to scrutinizer
add cli tool to show deleted users
uses are necessary after recent namespace change
also remove user from mappings table on deletion
add occ command to delete users
fix use statement
improve output
big fixes / improvements
PHP doc
return true in userExists early for cleaning up deleted users
bump version
control state and interval with one config.php setting, now ldapUserCleanupInterval. 0 will disable it. enabled by default.
improve doc
rename cli method to be consistent with others
introduce ldapUserCleanupInterval in sample config
don't show last login as unix epoche start when no login happend
less log output
consistent namespace for OfflineUser
rename GarbageCollector to DeletedUsersIndex and move it to user subdir
fix unit tests
add tests for deleteUser
more test adjustements
Conflicts:
apps/user_ldap/ajax/clearMappings.php
apps/user_ldap/appinfo/app.php
apps/user_ldap/lib/access.php
apps/user_ldap/lib/helper.php
apps/user_ldap/tests/helper.php
core/register_command.php
lib/private/preferences.php
lib/private/user.php
add ldap:check-user to check user existance on the fly
Conflicts:
apps/user_ldap/lib/helper.php
forgotten file
PHPdoc fixes, no code change
and don't forget to adjust tests
33 files changed, 1257 insertions, 66 deletions
diff --git a/apps/user_ldap/ajax/deleteConfiguration.php b/apps/user_ldap/ajax/deleteConfiguration.php index bca687c81ab..d409d891f61 100644 --- a/apps/user_ldap/ajax/deleteConfiguration.php +++ b/apps/user_ldap/ajax/deleteConfiguration.php @@ -27,7 +27,8 @@ OCP\JSON::checkAppEnabled('user_ldap'); OCP\JSON::callCheck(); $prefix = $_POST['ldap_serverconfig_chooser']; -if(\OCA\user_ldap\lib\Helper::deleteServerConfiguration($prefix)) { +$helper = new \OCA\user_ldap\lib\Helper(); +if($helper->deleteServerConfiguration($prefix)) { OCP\JSON::success(); } else { $l = \OC::$server->getL10N('user_ldap'); diff --git a/apps/user_ldap/ajax/getNewServerConfigPrefix.php b/apps/user_ldap/ajax/getNewServerConfigPrefix.php index 1c68b2e9a76..ce6c5ae76e8 100644 --- a/apps/user_ldap/ajax/getNewServerConfigPrefix.php +++ b/apps/user_ldap/ajax/getNewServerConfigPrefix.php @@ -26,7 +26,8 @@ OCP\JSON::checkAdminUser(); OCP\JSON::checkAppEnabled('user_ldap'); OCP\JSON::callCheck(); -$serverConnections = \OCA\user_ldap\lib\Helper::getServerConfigurationPrefixes(); +$helper = new \OCA\user_ldap\lib\Helper(); +$serverConnections = $helper->getServerConfigurationPrefixes(); sort($serverConnections); $lk = array_pop($serverConnections); $ln = intval(str_replace('s', '', $lk)); diff --git a/apps/user_ldap/appinfo/app.php b/apps/user_ldap/appinfo/app.php index 98d5fb60183..302575c3682 100644 --- a/apps/user_ldap/appinfo/app.php +++ b/apps/user_ldap/appinfo/app.php @@ -5,6 +5,7 @@ * * @author Dominik Schmidt * @copyright 2011 Dominik Schmidt dev@dominik-schmidt.de +* @copyright 2014 Arthur Schiwon <blizzz@owncloud.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -23,7 +24,8 @@ OCP\App::registerAdmin('user_ldap', 'settings'); -$configPrefixes = OCA\user_ldap\lib\Helper::getServerConfigurationPrefixes(true); +$helper = new \OCA\user_ldap\lib\Helper(); +$configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldapWrapper = new OCA\user_ldap\lib\LDAP(); if(count($configPrefixes) === 1) { $ocConfig = \OC::$server->getConfig(); @@ -50,16 +52,10 @@ if(count($configPrefixes) > 0) { OC_Group::useBackend($groupBackend); } -// add settings page to navigation -$entry = array( - 'id' => 'user_ldap_settings', - 'order'=>1, - 'href' => OCP\Util::linkTo( 'user_ldap', 'settings.php' ), - 'name' => 'LDAP' -); OCP\Util::addTranslations('user_ldap'); - OCP\Backgroundjob::registerJob('OCA\user_ldap\lib\Jobs'); +OCP\Backgroundjob::registerJob('\OCA\User_LDAP\Jobs\CleanUp'); + if(OCP\App::isEnabled('user_webdavauth')) { OCP\Util::writeLog('user_ldap', 'user_ldap and user_webdavauth are incompatible. You may experience unexpected behaviour', diff --git a/apps/user_ldap/appinfo/register_command.php b/apps/user_ldap/appinfo/register_command.php index 1a0227db95e..55a187d9654 100644 --- a/apps/user_ldap/appinfo/register_command.php +++ b/apps/user_ldap/appinfo/register_command.php @@ -6,9 +6,22 @@ * See the COPYING-README file. */ +use OCA\user_ldap\lib\Helper; +use OCA\user_ldap\lib\LDAP; +use OCA\user_ldap\User_Proxy; + $application->add(new OCA\user_ldap\Command\ShowConfig()); $application->add(new OCA\user_ldap\Command\SetConfig()); $application->add(new OCA\user_ldap\Command\TestConfig()); $application->add(new OCA\user_ldap\Command\CreateEmptyConfig()); $application->add(new OCA\user_ldap\Command\DeleteConfig()); $application->add(new OCA\user_ldap\Command\Search()); +$application->add(new OCA\user_ldap\Command\ShowRemnants()); +$helper = new OCA\user_ldap\lib\Helper(); +$uBackend = new OCA\user_ldap\User_Proxy( + $helper->getServerConfigurationPrefixes(true), + new OCA\user_ldap\lib\LDAP() +); +$application->add(new OCA\user_ldap\Command\CheckUser( + $uBackend, $helper, \OC::$server->getConfig() +)); diff --git a/apps/user_ldap/appinfo/update.php b/apps/user_ldap/appinfo/update.php index 9bf0ca4ab53..b4121b19852 100644 --- a/apps/user_ldap/appinfo/update.php +++ b/apps/user_ldap/appinfo/update.php @@ -12,7 +12,8 @@ if($state === 'unset') { $installedVersion = $configInstance->getAppValue('user_ldap', 'installed_version'); $enableRawMode = version_compare($installedVersion, '0.4.1', '<'); -$configPrefixes = OCA\user_ldap\lib\Helper::getServerConfigurationPrefixes(true); +$helper = new \OCA\user_ldap\lib\Helper(); +$configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldap = new OCA\user_ldap\lib\LDAP(); foreach($configPrefixes as $config) { $connection = new OCA\user_ldap\lib\Connection($ldap, $config); diff --git a/apps/user_ldap/appinfo/version b/apps/user_ldap/appinfo/version index 6f2743d65dc..0bfccb08040 100644 --- a/apps/user_ldap/appinfo/version +++ b/apps/user_ldap/appinfo/version @@ -1 +1 @@ -0.4.4 +0.4.5 diff --git a/apps/user_ldap/command/checkuser.php b/apps/user_ldap/command/checkuser.php new file mode 100644 index 00000000000..96c6c832356 --- /dev/null +++ b/apps/user_ldap/command/checkuser.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\user_ldap\Command; + +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; + +use OCA\user_ldap\lib\user\User; +use OCA\User_LDAP\lib\user\Manager; +use OCA\user_ldap\lib\Helper; +use OCA\user_ldap\User_Proxy; + +class CheckUser extends Command { + /** @var \OCA\user_ldap\User_Proxy */ + protected $backend; + + /** @var \OCA\User_LDAP\lib\Helper */ + protected $helper; + + /** @var \OCP\IConfig */ + protected $config; + + /** + * @param OCA\user_ldap\User_Proxy $uBackend + * @param OCA\User_LDAP\lib\Helper $helper + * @param OCP\IConfig $config + */ + public function __construct(User_Proxy $uBackend, Helper $helper, \OCP\IConfig $config) { + $this->backend = $uBackend; + $this->helper = $helper; + $this->config = $config; + parent::__construct(); + } + + protected function configure() { + $this + ->setName('ldap:check-user') + ->setDescription('checks whether a user exists on LDAP.') + ->addArgument( + 'ocName', + InputArgument::REQUIRED, + 'the user name as used in ownCloud' + ) + ->addOption( + 'force', + null, + InputOption::VALUE_NONE, + 'ignores disabled LDAP configuration' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) { + try { + $uid = $input->getArgument('ocName'); + $this->isAllowed($input->getOption('force')); + $this->confirmUserIsMapped($uid); + $exists = $this->backend->userExistsOnLDAP($uid); + if($exists === true) { + $output->writeln('The user is still available on LDAP.'); + return; + } + + // TODO FIXME consolidate next line in DeletedUsersIndex + // (impractical now, because of class dependencies) + $this->config->setUserValue($uid, 'user_ldap', 'isDeleted', '1'); + + $output->writeln('The user does not exists on LDAP anymore.'); + $output->writeln('Clean up the user\'s remnants by: ./occ user:delete "' + . $uid . '"'); + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage(). '</error>'); + } + } + + /** + * checks whether a user is actually mapped + * @param string $ocName the username as used in ownCloud + * @throws \Exception + * @return bool + */ + protected function confirmUserIsMapped($ocName) { + //TODO FIXME this should go to Mappings in OC 8 + $db = \OC::$server->getDatabaseConnection(); + $query = $db->prepare(' + SELECT + `ldap_dn` AS `dn` + FROM `*PREFIX*ldap_user_mapping` + WHERE `owncloud_name` = ?' + ); + + $query->execute(array($ocName)); + $result = $query->fetchColumn(); + + if($result === false) { + throw new \Exception('The given user is not a recognized LDAP user.'); + } + + return true; + } + + /** + * checks whether the setup allows reliable checking of LDAP user existance + * @throws \Exception + * @return bool + */ + protected function isAllowed($force) { + if($this->helper->haveDisabledConfigurations() && !$force) { + throw new \Exception('Cannot check user existance, 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. + + return true; + } + +} diff --git a/apps/user_ldap/command/search.php b/apps/user_ldap/command/search.php index e20255510d8..d826303c55d 100644 --- a/apps/user_ldap/command/search.php +++ b/apps/user_ldap/command/search.php @@ -74,7 +74,8 @@ class Search extends Command { } protected function execute(InputInterface $input, OutputInterface $output) { - $configPrefixes = Helper::getServerConfigurationPrefixes(true); + $helper = new Helper(); + $configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldapWrapper = new LDAP(); $offset = intval($input->getOption('offset')); diff --git a/apps/user_ldap/command/setconfig.php b/apps/user_ldap/command/setconfig.php index ab1c8d39ead..9128fcf04fc 100644 --- a/apps/user_ldap/command/setconfig.php +++ b/apps/user_ldap/command/setconfig.php @@ -41,7 +41,8 @@ class SetConfig extends Command { } protected function execute(InputInterface $input, OutputInterface $output) { - $availableConfigs = Helper::getServerConfigurationPrefixes(); + $helper = new Helper(); + $availableConfigs = $helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); if(!in_array($configID, $availableConfigs)) { $output->writeln("Invalid configID"); diff --git a/apps/user_ldap/command/showconfig.php b/apps/user_ldap/command/showconfig.php index f51d641beec..ddbc45243ff 100644 --- a/apps/user_ldap/command/showconfig.php +++ b/apps/user_ldap/command/showconfig.php @@ -31,7 +31,8 @@ class ShowConfig extends Command { } protected function execute(InputInterface $input, OutputInterface $output) { - $availableConfigs = Helper::getServerConfigurationPrefixes(); + $helper = new Helper(); + $availableConfigs = $helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); if(!is_null($configID)) { $configIDs[] = $configID; diff --git a/apps/user_ldap/command/showremnants.php b/apps/user_ldap/command/showremnants.php new file mode 100644 index 00000000000..3d39f977421 --- /dev/null +++ b/apps/user_ldap/command/showremnants.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\user_ldap\Command; + +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; + +use OCA\user_ldap\lib\user\DeletedUsersIndex; +use OCA\User_LDAP\lib\Connection; +use OCA\User_LDAP\lib\Access; + +class ShowRemnants extends Command { + + protected function configure() { + $this + ->setName('ldap:show-remnants') + ->setDescription('shows which users are not available on LDAP anymore, but have remnants in ownCloud.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $dui = new DeletedUsersIndex( + new \OC\Preferences(\OC_DB::getConnection()), + \OC::$server->getDatabaseConnection(), + $this->getAccess() + ); + + /** @var \Symfony\Component\Console\Helper\Table $table */ + $table = $this->getHelperSet()->get('table'); + $table->setHeaders(array( + 'ownCloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login', + 'Dir', 'Sharer')); + $rows = array(); + $offset = 0; + do { + $resultSet = $dui->getUsers($offset); + $offset += count($resultSet); + foreach($resultSet as $user) { + $hAS = $user->getHasActiveShares() ? 'Y' : 'N'; + $lastLogin = ($user->getLastLogin() > 0) ? + \OCP\Util::formatDate($user->getLastLogin()) : '-'; + $rows[] = array( + $user->getOCName(), + $user->getDisplayName(), + $user->getUid(), + $user->getDN(), + $lastLogin, + $user->getHomePath(), + $hAS + ); + } + } while (count($resultSet) === 10); + + $table->setRows($rows); + $table->render($output); + } + + protected function getAccess() { + $ldap = new \OCA\user_ldap\lib\LDAP(); + $dummyConnection = new Connection($ldap, '', null); + $userManager = new \OCA\user_ldap\lib\user\Manager( + \OC::$server->getConfig(), + new \OCA\user_ldap\lib\FilesystemHelper(), + new \OCA\user_ldap\lib\LogWrapper(), + \OC::$server->getAvatarManager(), + new \OCP\Image() + ); + $access = new Access($dummyConnection, $ldap, $userManager); + return $access; + } + +} diff --git a/apps/user_ldap/command/testconfig.php b/apps/user_ldap/command/testconfig.php index 00b4acf2f66..a44e22415e9 100644 --- a/apps/user_ldap/command/testconfig.php +++ b/apps/user_ldap/command/testconfig.php @@ -31,7 +31,8 @@ class TestConfig extends Command { } protected function execute(InputInterface $input, OutputInterface $output) { - $availableConfigs = Helper::getServerConfigurationPrefixes(); + $helper = new Helper(); + $availableConfigs = $helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); if(!in_array($configID, $availableConfigs)) { $output->writeln("Invalid configID"); diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index 5d0910320bf..692afb98f99 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -290,6 +290,7 @@ class Access extends LDAPUtility implements user\IUserTools { } /** + public function ocname2dn($name, $isUser) { * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure * @param string $fdn the dn of the group object * @param string $ldapName optional, the display name of the object diff --git a/apps/user_ldap/lib/connection.php b/apps/user_ldap/lib/connection.php index 54aafb93410..5df5031d001 100644 --- a/apps/user_ldap/lib/connection.php +++ b/apps/user_ldap/lib/connection.php @@ -71,8 +71,9 @@ class Connection extends LDAPUtility { } $this->hasPagedResultSupport = $this->ldap->hasPagedResultSupport(); + $helper = new Helper(); $this->doNotValidate = !in_array($this->configPrefix, - Helper::getServerConfigurationPrefixes()); + $helper->getServerConfigurationPrefixes()); } public function __destruct() { diff --git a/apps/user_ldap/lib/helper.php b/apps/user_ldap/lib/helper.php index fa36e304171..7a96cfa36c4 100644 --- a/apps/user_ldap/lib/helper.php +++ b/apps/user_ldap/lib/helper.php @@ -45,7 +45,7 @@ class Helper { * except the default (first) server shall be connected to. * */ - static public function getServerConfigurationPrefixes($activeConfigurations = false) { + public function getServerConfigurationPrefixes($activeConfigurations = false) { $referenceConfigkey = 'ldap_configuration_active'; $sql = ' @@ -83,7 +83,7 @@ class Helper { * @return array an array with configprefix as keys * */ - static public function getServerConfigurationHosts() { + public function getServerConfigurationHosts() { $referenceConfigkey = 'ldap_host'; $query = ' @@ -110,7 +110,7 @@ class Helper { * @param string $prefix the configuration prefix of the config to delete * @return bool true on success, false otherwise */ - static public function deleteServerConfiguration($prefix) { + public function deleteServerConfiguration($prefix) { if(!in_array($prefix, self::getServerConfigurationPrefixes())) { return false; } @@ -142,11 +142,27 @@ class Helper { } /** + * checks whether there is one or more disabled LDAP configurations + * @throws \Exception + * @return bool + */ + public function haveDisabledConfigurations() { + $all = $this->getServerConfigurationPrefixes(false); + $active = $this->getServerConfigurationPrefixes(true); + + if(!is_array($all) || !is_array($active)) { + throw new \Exception('Unexpected Return Value'); + } + + 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 */ - static public function getDomainFromURL($url) { + public function getDomainFromURL($url) { $uinfo = parse_url($url); if(!is_array($uinfo)) { return false; diff --git a/apps/user_ldap/lib/jobs.php b/apps/user_ldap/lib/jobs.php index 47e536f8f64..30f09cdc8f8 100644 --- a/apps/user_ldap/lib/jobs.php +++ b/apps/user_ldap/lib/jobs.php @@ -156,7 +156,8 @@ class Jobs extends \OC\BackgroundJob\TimedJob { if(!is_null(self::$groupBE)) { return self::$groupBE; } - $configPrefixes = Helper::getServerConfigurationPrefixes(true); + $helper = new Helper(); + $configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldapWrapper = new LDAP(); if(count($configPrefixes) === 1) { //avoid the proxy when there is only one LDAP server configured diff --git a/apps/user_ldap/lib/jobs/cleanup.php b/apps/user_ldap/lib/jobs/cleanup.php new file mode 100644 index 00000000000..56fb296609d --- /dev/null +++ b/apps/user_ldap/lib/jobs/cleanup.php @@ -0,0 +1,227 @@ +<?php +/** + * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\User_LDAP\Jobs; + +use \OCA\user_ldap\User_Proxy; +use \OCA\user_ldap\lib\Helper; +use \OCA\user_ldap\lib\LDAP; + +/** + * Class CleanUp + * + * a Background job to clean up deleted users + * + * @package OCA\user_ldap\lib; + */ +class CleanUp extends \OC\BackgroundJob\TimedJob { + /** + * @var int $limit amount of users that should be checked per run + */ + protected $limit = 50; + + /** + * @var \OCP\UserInterface $userBackend + */ + protected $userBackend; + + /** + * @var \OCP\IConfig $ocConfig + */ + protected $ocConfig; + + /** + * @var \OCP\IDBConnection $db + */ + protected $db; + + /** + * @var Helper $ldapHelper + */ + protected $ldapHelper; + + /** + * @var int $defaultIntervalMin default interval in minutes + */ + protected $defaultIntervalMin = 51; + + public function __construct() { + $minutes = \OC::$server->getConfig()->getSystemValue( + 'ldapUserCleanupInterval', strval($this->defaultIntervalMin)); + $this->setInterval(intval($minutes) * 60); + } + + /** + * assigns the instances passed to run() to the class properties + * @param array $arguments + */ + public function setArguments($arguments) { + //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(); + } + + if(isset($arguments['userBackend'])) { + $this->userBackend = $arguments['userBackend']; + } else { + $this->userBackend = new User_Proxy( + $this->ldapHelper->getServerConfigurationPrefixes(true), + new LDAP() + ); + } + + if(isset($arguments['ocConfig'])) { + $this->ocConfig = $arguments['ocConfig']; + } else { + $this->ocConfig = \OC::$server->getConfig(); + } + + if(isset($arguments['db'])) { + $this->db = $arguments['db']; + } else { + $this->db = \OC::$server->getDatabaseConnection(); + } + } + + /** + * makes the background job do its work + * @param array $argument + */ + public function run($argument) { + $this->setArguments($argument); + + if(!$this->isCleanUpAllowed()) { + return; + } + $users = $this->getMappedUsers($this->limit, $this->getOffset()); + if(!is_array($users)) { + //something wrong? Let's start from the beginning next time and + //abort + $this->setOffset(true); + return; + } + $resetOffset = $this->isOffsetResetNecessary(count($users)); + $this->checkUsers($users); + $this->setOffset($resetOffset); + } + + /** + * checks whether next run should start at 0 again + * @param int $resultCount + * @return bool + */ + public function isOffsetResetNecessary($resultCount) { + return ($resultCount < $this->limit) ? true : false; + } + + /** + * checks whether cleaning up LDAP users is allowed + * @return bool + */ + public function isCleanUpAllowed() { + try { + if($this->ldapHelper->haveDisabledConfigurations()) { + return false; + } + } catch (\Exception $e) { + return false; + } + + $enabled = $this->isCleanUpEnabled(); + + return $enabled; + } + + /** + * checks whether clean up is enabled by configuration + * @return bool + */ + private function isCleanUpEnabled() { + return (bool)$this->ocConfig->getSystemValue( + 'ldapUserCleanupInterval', strval($this->defaultIntervalMin)); + } + + /** + * checks users whether they are still existing + * @param array $users result from getMappedUsers() + */ + private function checkUsers($users) { + foreach($users as $user) { + $this->checkUser($user); + } + } + + /** + * checks whether a user is still existing in LDAP + * @param string[] $user + */ + private function checkUser($user) { + if($this->userBackend->userExistsOnLDAP($user['name'])) { + //still available, all good + return; + } + + // TODO FIXME consolidate next line in DeletedUsersIndex + // (impractical now, because of class dependencies) + $this->ocConfig->setUserValue($user['name'], 'user_ldap', 'isDeleted', '1'); + } + + /** + * returns a batch of users from the mappings table + * @param int $limit + * @param int $offset + * @return array + */ + public function getMappedUsers($limit, $offset) { + $query = $this->db->prepare(' + SELECT + `ldap_dn` AS `dn`, + `owncloud_name` AS `name`, + `directory_uuid` AS `uuid` + FROM `*PREFIX*ldap_user_mapping`', + $limit, + $offset + ); + + $query->execute(); + return $query->fetchAll(); + } + + /** + * gets the offset to fetch users from the mappings table + * @return int + */ + private function getOffset() { + return $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($reset = false) { + $newOffset = $reset ? 0 : + $this->getOffset() + $this->limit; + $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', $newOffset); + } + + /** + * returns the chunk size (limit in DB speak) + * @return int + */ + public function getChunkSize() { + return $this->limit; + } + +} diff --git a/apps/user_ldap/lib/user/deletedusersindex.php b/apps/user_ldap/lib/user/deletedusersindex.php new file mode 100644 index 00000000000..0d8bacffe94 --- /dev/null +++ b/apps/user_ldap/lib/user/deletedusersindex.php @@ -0,0 +1,125 @@ +<?php + +/** + * ownCloud – LDAP Helper + * + * @author Arthur Schiwon + * @copyright 2014 Arthur Schiwon <blizzz@owncloud.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\user_ldap\lib\user; + +use OCA\user_ldap\lib\user\OfflineUser; +use OCA\user_ldap\lib\Access; + +/** + * Class DeletedUsersIndex + * @package OCA\User_LDAP + */ +class DeletedUsersIndex { + /** + * @var \OC\Preferences $preferences + */ + protected $preferences; + + /** + * @var \OCP\IDBConnection $db + */ + protected $db; + + /** + * @var \OCA\user_ldap\lib\Access $access + */ + protected $access; + + /** + * @var int $limit + */ + protected $limit = 10; + + /** + * @var array $deletedUsers + */ + protected $deletedUsers = false; + + public function __construct(\OC\Preferences $preferences, \OCP\IDBConnection $db, Access $access) { + $this->preferences = $preferences; + $this->db = $db; + $this->access = $access; + } + + /** + * returns key to be used against $this->deletedUsers + * @param int $limit + * @param int $offset + * @return string + */ + private function getDeletedUsersCacheKey($limit, $offset) { + return strval($limit) . '.' . strval($offset); + } + + /** + * reads LDAP users marked as deleted from the database + * @param int $offset + * @return OCA\user_ldap\lib\user\OfflineUser[] + */ + private function fetchDeletedUsers($offset) { + $deletedUsers = $this->preferences->getUsersForValue( + 'user_ldap', 'isDeleted', '1', $this->limit, $offset); + $key = $this->getDeletedUsersCacheKey($this->limit, $offset); + + $userObjects = array(); + foreach($deletedUsers as $user) { + $userObjects[] = new OfflineUser($user, $this->preferences, $this->db, $this->access); + } + + $this->deletedUsers[$key] = $userObjects; + if(count($userObjects) > 0) { + $this->hasUsers(); + } + return $this->deletedUsers[$key]; + } + + /** + * returns all LDAP users that are marked as deleted + * @param int|null $offset + * @return OCA\user_ldap\lib\user\OfflineUser[] + */ + public function getUsers($offset = null) { + $key = $this->getDeletedUsersCacheKey($this->limit, $offset); + if(is_array($this->deletedUsers) && isset($this->deletedUsers[$key])) { + return $this->deletedUsers[$key]; + } + return $this->fetchDeletedUsers($offset); + } + + /** + * whether at least one user was detected as deleted + * @return bool + */ + public function hasUsers() { + if($this->deletedUsers === false) { + $this->fetchDeletedUsers(0); + } + foreach($this->deletedUsers as $batch) { + if(count($batch) > 0) { + return true; + } + } + return false; + } +} diff --git a/apps/user_ldap/lib/user/iusertools.php b/apps/user_ldap/lib/user/iusertools.php index bbc678153de..ffdef62410d 100644 --- a/apps/user_ldap/lib/user/iusertools.php +++ b/apps/user_ldap/lib/user/iusertools.php @@ -39,4 +39,7 @@ interface IUserTools { public function username2dn($name); + //temporary hack for LDAP user cleanup, will be removed in OC 8. + public function ocname2dn($name, $isUser); + } diff --git a/apps/user_ldap/lib/user/manager.php b/apps/user_ldap/lib/user/manager.php index 0ed3d09c48f..1bcc9b96d8a 100644 --- a/apps/user_ldap/lib/user/manager.php +++ b/apps/user_ldap/lib/user/manager.php @@ -27,6 +27,7 @@ use OCA\user_ldap\lib\user\IUserTools; use OCA\user_ldap\lib\user\User; use OCA\user_ldap\lib\LogWrapper; use OCA\user_ldap\lib\FilesystemHelper; +use OCA\user_ldap\lib\user\OfflineUser; /** * Manager @@ -60,7 +61,9 @@ class Manager { */ protected $avatarManager; /** - * @var string[][] + * array['byDN'] \OCA\user_ldap\lib\User[] + * ['byUid'] \OCA\user_ldap\lib\User[] + * @var array $users */ protected $users = array( 'byDN' => array(), @@ -131,9 +134,45 @@ class Manager { } /** + * Checks whether the specified user is marked as deleted + * @param string $id the ownCloud user name + * @return bool + */ + public function isDeletedUser($id) { + $isDeleted = $this->ocConfig->getUserValue( + $id, 'user_ldap', 'isDeleted', 0); + return intval($isDeleted) === 1; + } + + /** + * creates and returns an instance of OfflineUser for the specified user + * @param string $id + * @return \OCA\user_ldap\lib\user\OfflineUser + */ + public function getDeletedUser($id) { + return new OfflineUser( + $id, + new \OC\Preferences(\OC_DB::getConnection()), + \OC::$server->getDatabaseConnection(), + $this->access); + } + + 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); + } + throw new \Exception('Could not create User instance'); + } + + /** * @brief returns a User object by it's DN or ownCloud username * @param string the DN or username of the user - * @return \OCA\user_ldap\lib\User | null + * @return \OCA\user_ldap\lib\user\User|\OCA\user_ldap\lib\user\OfflineUser|null */ public function get($id) { $this->checkAccess(); @@ -143,25 +182,19 @@ class Manager { return $this->users['byUid'][$id]; } - if(!$this->access->stringResemblesDN($id) ) { - //most likely a uid - $dn = $this->access->username2dn($id); - if($dn !== false) { - return $this->createAndCache($dn, $id); - } - } else { - //so it's a DN + if($this->access->stringResemblesDN($id) ) { $uid = $this->access->dn2username($id); if($uid !== false) { return $this->createAndCache($id, $uid); } } - //either funny uid or invalid. Assume funny to be on the safe side. - $dn = $this->access->username2dn($id); - if($dn !== false) { - return $this->createAndCache($dn, $id); + + try { + $user = $this->createInstancyByUserName($id); + return $user; + } catch (\Exception $e) { + return null; } - return null; } } diff --git a/apps/user_ldap/lib/user/offlineuser.php b/apps/user_ldap/lib/user/offlineuser.php new file mode 100644 index 00000000000..7750348a280 --- /dev/null +++ b/apps/user_ldap/lib/user/offlineuser.php @@ -0,0 +1,217 @@ +<?php + +/** + * ownCloud – LDAP User + * + * @author Arthur Schiwon + * @copyright 2014 Arthur Schiwon blizzz@owncloud.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\user_ldap\lib\user; + +use OCA\user_ldap\lib\Access; + +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 $email + */ + protected $email; + /** + * @var bool $hasActiveShares + */ + protected $hasActiveShares; + /** + * @var \OC\Preferences $preferences + */ + protected $preferences; + /** + * @var \OCP\IDBConnection $db + */ + protected $db; + /** + * @var \OCA\user_ldap\lib\Access + */ + protected $access; + + public function __construct($ocName, \OC\Preferences $preferences, \OCP\IDBConnection $db, Access $access) { + $this->ocName = $ocName; + $this->preferences = $preferences; + $this->db = $db; + $this->access = $access; + $this->fetchDetails(); + } + + /** + * exports the user details in an assoc array + * @return array + */ + public function export() { + $data = array(); + $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 ownCloud internal name + * @return string + */ + public function getOCName() { + return $this->ocName; + } + + /** + * getter for LDAP uid + * @return string + */ + public function getUID() { + return $this->uid; + } + + /** + * getter for LDAP DN + * @return string + */ + public function getDN() { + return $this->dn; + } + + /** + * getter for display name + * @return string + */ + public function getDisplayName() { + return $this->displayName; + } + + /** + * getter for email + * @return string + */ + public function getEmail() { + return $this->email; + } + + /** + * getter for home directory path + * @return string + */ + public function getHomePath() { + return $this->homePath; + } + + /** + * getter for the last login timestamp + * @return int + */ + public function getLastLogin() { + return intval($this->lastLogin); + } + + /** + * getter for having active shares + * @return bool + */ + public function getHasActiveShares() { + return $this->hasActiveShares; + } + + /** + * reads the user details + */ + protected function fetchDetails() { + $properties = array ( + 'displayName' => 'user_ldap', + 'uid' => 'user_ldap', + 'homePath' => 'user_ldap', + 'email' => 'settings', + 'lastLogin' => 'login' + ); + foreach($properties as $property => $app) { + $this->$property = $this->preferences->getValue($this->ocName, $app, $property, ''); + } + + $dn = $this->access->ocname2dn($this->ocName, true); + $this->dn = ($dn !== false) ? $dn : ''; + + $this->determineShares(); + } + + + /** + * finds out whether the user has active shares. The result is stored in + * $this->hasActiveShares + */ + protected function determineShares() { + $query = $this->db->prepare(' + SELECT COUNT(`uid_owner`) + FROM `*PREFIX*share` + WHERE `uid_owner` = ? + ', 1); + $query->execute(array($this->ocName)); + $sResult = $query->fetchColumn(0); + if(intval($sResult) === 1) { + $this->hasActiveShares = true; + return; + } + + $query = $this->db->prepare(' + SELECT COUNT(`owner`) + FROM `*PREFIX*share_external` + WHERE `owner` = ? + ', 1); + $query->execute(array($this->ocName)); + $sResult = $query->fetchColumn(0); + if(intval($sResult) === 1) { + $this->hasActiveShares = true; + return; + } + + $this->hasActiveShares = false; + } +} diff --git a/apps/user_ldap/lib/user/user.php b/apps/user_ldap/lib/user/user.php index d4d2294307d..c81fb25b541 100644 --- a/apps/user_ldap/lib/user/user.php +++ b/apps/user_ldap/lib/user/user.php @@ -213,6 +213,31 @@ class User { } /** + * 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); + } + + /** + * Stores the display name in the databae + * @param string $displayName + */ + public function storeDisplayName($displayName) { + $this->store('displayName', $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. diff --git a/apps/user_ldap/lib/wizard.php b/apps/user_ldap/lib/wizard.php index 578a920f00e..2e4507a2585 100644 --- a/apps/user_ldap/lib/wizard.php +++ b/apps/user_ldap/lib/wizard.php @@ -659,7 +659,8 @@ class Wizard extends LDAPUtility { //this did not help :( //Let's see whether we can parse the Host URL and convert the domain to //a base DN - $domain = Helper::getDomainFromURL($this->configuration->ldapHost); + $helper = new Helper(); + $domain = $helper->getDomainFromURL($this->configuration->ldapHost); if(!$domain) { return false; } diff --git a/apps/user_ldap/settings.php b/apps/user_ldap/settings.php index 5527cf2c6da..a19ec0bda6f 100644 --- a/apps/user_ldap/settings.php +++ b/apps/user_ldap/settings.php @@ -35,8 +35,9 @@ OCP\Util::addStyle('user_ldap', 'settings'); // fill template $tmpl = new OCP\Template('user_ldap', 'settings'); -$prefixes = \OCA\user_ldap\lib\Helper::getServerConfigurationPrefixes(); -$hosts = \OCA\user_ldap\lib\Helper::getServerConfigurationHosts(); +$helper = new \OCA\user_ldap\lib\Helper(); +$prefixes = $helper->getServerConfigurationPrefixes(); +$hosts = $helper->getServerConfigurationHosts(); $wizardHtml = ''; $toc = array(); diff --git a/apps/user_ldap/tests/jobs/cleanup.php b/apps/user_ldap/tests/jobs/cleanup.php new file mode 100644 index 00000000000..3aa9a4a43c5 --- /dev/null +++ b/apps/user_ldap/tests/jobs/cleanup.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\user_ldap\tests; + +class Test_CleanUp extends \PHPUnit_Framework_TestCase { + public function getMocks() { + $mocks = array(); + $mocks['userBackend'] = + $this->getMockBuilder('\OCA\user_ldap\User_Proxy') + ->disableOriginalConstructor() + ->getMock(); + $mocks['ocConfig'] = $this->getMock('\OCP\IConfig'); + $mocks['db'] = $this->getMock('\OCP\IDBConnection'); + $mocks['helper'] = $this->getMock('\OCA\user_ldap\lib\Helper'); + + return $mocks; + } + + /** + * clean up job must not run when there are disabled configurations + */ + public function test_runNotAllowedByDisabledConfigurations() { + $args = $this->getMocks(); + $args['helper']->expects($this->once()) + ->method('haveDisabledConfigurations') + ->will($this->returnValue(true) ); + + $args['ocConfig']->expects($this->never()) + ->method('getSystemValue'); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isCleanUpAllowed(); + $this->assertSame(false, $result); + } + + /** + * clean up job must not run when LDAP Helper is broken i.e. + * returning unexpected results + */ + public function test_runNotAllowedByBrokenHelper() { + $args = $this->getMocks(); + $args['helper']->expects($this->once()) + ->method('haveDisabledConfigurations') + ->will($this->throwException(new \Exception())); + + $args['ocConfig']->expects($this->never()) + ->method('getSystemValue'); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isCleanUpAllowed(); + $this->assertSame(false, $result); + } + + /** + * clean up job must not run when it is not enabled + */ + public function test_runNotAllowedBySysConfig() { + $args = $this->getMocks(); + $args['helper']->expects($this->once()) + ->method('haveDisabledConfigurations') + ->will($this->returnValue(false)); + + $args['ocConfig']->expects($this->once()) + ->method('getSystemValue') + ->will($this->returnValue(false)); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isCleanUpAllowed(); + $this->assertSame(false, $result); + } + + /** + * clean up job is allowed to run + */ + public function test_runIsAllowed() { + $args = $this->getMocks(); + $args['helper']->expects($this->once()) + ->method('haveDisabledConfigurations') + ->will($this->returnValue(false)); + + $args['ocConfig']->expects($this->once()) + ->method('getSystemValue') + ->will($this->returnValue(true)); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isCleanUpAllowed(); + $this->assertSame(true, $result); + } + + /** + * test whether sql is OK + */ + public function test_getMappedUsers() { + $args = $this->getMocks(); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + if(version_compare(\PHPUnit_Runner_Version::id(), '3.8', '<')) { + //otherwise we run into + //https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 + $this->markTestIncomplete(); + } + + $stmt = $this->getMock('\Doctrine\DBAL\Driver\Statement'); + + $args['db']->expects($this->once()) + ->method('prepare') + ->will($this->returnValue($stmt)); + + $bgJob->getMappedUsers(0, $bgJob->getChunkSize()); + } + + /** + * check whether offset will be reset when it needs to + */ + public function test_OffsetResetIsNecessary() { + $args = $this->getMocks(); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isOffsetResetNecessary($bgJob->getChunkSize() - 1); + $this->assertSame(true, $result); + } + + /** + * make sure offset is not reset when it is not due + */ + public function test_OffsetResetIsNotNecessary() { + $args = $this->getMocks(); + + $bgJob = new \OCA\User_LDAP\Jobs\CleanUp(); + $bgJob->setArguments($args); + + $result = $bgJob->isOffsetResetNecessary($bgJob->getChunkSize()); + $this->assertSame(false, $result); + } + +} + diff --git a/apps/user_ldap/tests/user/manager.php b/apps/user_ldap/tests/user/manager.php index b3e52084dba..fb47f60539f 100644 --- a/apps/user_ldap/tests/user/manager.php +++ b/apps/user_ldap/tests/user/manager.php @@ -183,7 +183,7 @@ class Test_User_Manager extends \Test\TestCase { $access->expects($this->never()) ->method('dn2username'); - $access->expects($this->exactly(2)) + $access->expects($this->exactly(1)) ->method('username2dn') ->with($this->equalTo($uid)) ->will($this->returnValue(false)); diff --git a/apps/user_ldap/tests/user_ldap.php b/apps/user_ldap/tests/user_ldap.php index 33cec0247b6..876b3d0903a 100644 --- a/apps/user_ldap/tests/user_ldap.php +++ b/apps/user_ldap/tests/user_ldap.php @@ -123,7 +123,7 @@ class Test_User_Ldap_Direct extends \Test\TestCase { ->method('fetchListOfUsers') ->will($this->returnCallback(function($filter) { if($filter === 'roland') { - return array('dnOfRoland,dc=test'); + return array(array('dn' => 'dnOfRoland,dc=test')); } return array(); })); @@ -230,6 +230,24 @@ class Test_User_Ldap_Direct extends \Test\TestCase { $this->assertFalse($result); } + public function testDeleteUserCancel() { + $access = $this->getAccessMock(); + $backend = new UserLDAP($access); + $result = $backend->deleteUser('notme'); + $this->assertFalse($result); + } + + public function testDeleteUserSuccess() { + $access = $this->getAccessMock(); + $backend = new UserLDAP($access); + + $pref = \OC::$server->getConfig(); + $pref->setUserValue('jeremy', 'user_ldap', 'isDeleted', 1); + + $result = $backend->deleteUser('jeremy'); + $this->assertTrue($result); + } + /** * Prepares the Access mock for getUsers tests * @param \OCA\user_ldap\lib\Access $access mock diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php index 482715b3686..2274e4156cc 100644 --- a/apps/user_ldap/user_ldap.php +++ b/apps/user_ldap/user_ldap.php @@ -26,16 +26,23 @@ namespace OCA\user_ldap; use OCA\user_ldap\lib\BackendUtility; +use OCA\user_ldap\lib\user\OfflineUser; +use OCA\User_LDAP\lib\User\User; class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserInterface { /** + * @var string[] $homesToKill + */ + protected $homesToKill = array(); + + /** * checks whether the user is allowed to change his avatar in ownCloud * @param string $uid the ownCloud user name * @return boolean either the user can or cannot */ public function canChangeAvatar($uid) { $user = $this->access->userManager->get($uid); - if(is_null($user)) { + if(!$user instanceof User) { return false; } if($user->getAvatarImage() === false) { @@ -57,15 +64,17 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn $uid = $this->access->escapeFilterPart($uid); //find out dn of the user name + $attrs = array($this->access->connection->ldapUserDisplayName, 'dn', + 'uid', 'samaccountname'); $filter = \OCP\Util::mb_str_replace( '%uid', $uid, $this->access->connection->ldapLoginFilter, 'UTF-8'); - $ldap_users = $this->access->fetchListOfUsers($filter, 'dn'); - if(count($ldap_users) < 1) { + $users = $this->access->fetchListOfUsers($filter, $attrs); + if(count($users) < 1) { return false; } - $dn = $ldap_users[0]; + $dn = $users[0]['dn']; $user = $this->access->userManager->get($dn); - if(is_null($user)) { + if(!$user instanceof User) { \OCP\Util::writeLog('user_ldap', 'LDAP Login: Could not get user object for DN ' . $dn . '. Maybe the LDAP entry has no set display name attribute?', @@ -79,6 +88,15 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn } $user->markLogin(); + if(isset($users[0][$this->access->connection->ldapUserDisplayName])) { + $dpn = $users[0][$this->access->connection->ldapUserDisplayName]; + $user->storeDisplayName($dpn); + } + if(isset($users[0]['uid'])) { + $user->storeLDAPUserName($users[0]['uid']); + } else if(isset($users[0]['samaccountname'])) { + $user->storeLDAPUserName($users[0]['samaccountname']); + } return $user->getUsername(); } @@ -128,6 +146,33 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn } /** + * checks whether a user is still available on LDAP + * @param string|\OCA\User_LDAP\lib\user\User $user either the ownCloud user + * name or an instance of that user + * @return bool + */ + public function userExistsOnLDAP($user) { + if(is_string($user)) { + $user = $this->access->userManager->get($user); + } + if(!$user instanceof User) { + return false; + } + + $dn = $user->getDN(); + //check if user really still exists by reading its entry + if(!is_array($this->access->readAttribute($dn, ''))) { + $lcr = $this->access->connection->getConnectionResource(); + if(is_null($lcr)) { + throw new \Exception('No LDAP Connection to server ' . $this->access->connection->ldapHost); + } + return false; + } + + return true; + } + + /** * check if a user exists * @param string $uid the username * @return boolean @@ -143,36 +188,56 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn $this->access->connection->ldapHost, \OCP\Util::DEBUG); $this->access->connection->writeToCache('userExists'.$uid, false); return false; + } else if($user instanceof OfflineUser) { + //express check for users marked as deleted. Returning true is + //necessary for cleanup + return true; } - $dn = $user->getDN(); - //check if user really still exists by reading its entry - if(!is_array($this->access->readAttribute($dn, ''))) { - \OCP\Util::writeLog('user_ldap', 'LDAP says no user '.$dn.' on '. - $this->access->connection->ldapHost, \OCP\Util::DEBUG); - $this->access->connection->writeToCache('userExists'.$uid, false); + + try { + $result = $this->userExistsOnLDAP($user); + $this->access->connection->writeToCache('userExists'.$uid, $result); + if($result === true) { + $user->update(); + } + return $result; + } catch (\Exception $e) { + \OCP\Util::writeLog('user_ldap', $e->getMessage(), \OCP\Util::WARN); return false; } - - $this->access->connection->writeToCache('userExists'.$uid, true); - $user->update(); - return true; } /** - * delete a user + * returns whether a user was deleted in LDAP + * * @param string $uid The username of the user to delete * @return bool - * - * Deletes a user */ public function deleteUser($uid) { - return false; + $pref = \OC::$server->getConfig(); + $marked = $pref->getUserValue($uid, 'user_ldap', 'isDeleted', 0); + if(intval($marked) === 0) { + \OC::$server->getLogger()->notice( + 'User '.$uid . ' is not marked as deleted, not cleaning up.', + array('app' => 'user_ldap')); + return false; + } + \OC::$server->getLogger()->info('Cleaning up after user ' . $uid, + array('app' => 'user_ldap')); + + //Get Home Directory out of user preferences so we can return it later, + //necessary for removing directories as done by OC_User. + $home = $pref->getUserValue($uid, 'user_ldap', 'homePath', ''); + $this->homesToKill[$uid] = $home; + $this->access->unmapUser($uid); + + return true; } /** * get the user's home directory * @param string $uid the username - * @return boolean + * @return string|bool */ public function getHome($uid) { // user Exists check required as it is not done in user proxy! @@ -180,10 +245,16 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn return false; } + if(isset($this->homesToKill[$uid]) && !empty($this->homesToKill[$uid])) { + //a deleted user who needs some clean up + return $this->homesToKill[$uid]; + } + $cacheKey = 'getHome'.$uid; if($this->access->connection->isCached($cacheKey)) { return $this->access->connection->getFromCache($cacheKey); } + $pref = \OC::$server->getConfig(); if(strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0) { $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); $homedir = $this->access->readAttribute( @@ -203,12 +274,17 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn \OC::$SERVERROOT.'/data' ) . '/' . $homedir[0]; } $this->access->connection->writeToCache($cacheKey, $homedir); + //we need it to store it in the DB as well in case a user gets + //deleted so we can clean up afterwards + $pref->setUserValue($uid, 'user_ldap', 'homePath', $homedir); + //TODO: if home directory changes, the old one needs to be removed. return $homedir; } } //false will apply default behaviour as defined and done by OC_User $this->access->connection->writeToCache($cacheKey, false); + $pref->setUserValue($uid, 'user_ldap', 'homePath', ''); return false; } diff --git a/apps/user_ldap/user_proxy.php b/apps/user_ldap/user_proxy.php index 6414a048071..77caa84ecd9 100644 --- a/apps/user_ldap/user_proxy.php +++ b/apps/user_ldap/user_proxy.php @@ -24,6 +24,7 @@ namespace OCA\user_ldap; use OCA\user_ldap\lib\ILDAPWrapper; +use OCA\User_LDAP\lib\User\User; class User_Proxy extends lib\Proxy implements \OCP\IUserBackend, \OCP\UserInterface { private $backends = array(); @@ -153,6 +154,17 @@ class User_Proxy extends lib\Proxy implements \OCP\IUserBackend, \OCP\UserInterf } /** + * check if a user exists on LDAP + * @param string|OCA\User_LDAP\lib\User\User $user either the ownCloud user + * name or an instance of that user + * @return boolean + */ + public function userExistsOnLDAP($user) { + $id = ($user instanceof User) ? $user->getUsername() : $user; + return $this->handleRequest($id, 'userExistsOnLDAP', array($user)); + } + + /** * Check if the password is correct * @param string $uid The username * @param string $password The password @@ -217,7 +229,7 @@ class User_Proxy extends lib\Proxy implements \OCP\IUserBackend, \OCP\UserInterf * Deletes a user */ public function deleteUser($uid) { - return false; + return $this->handleRequest($uid, 'deleteUser', array($uid)); } /** diff --git a/config/config.sample.php b/config/config.sample.php index 35e3f6ce5f1..e5b8344ad37 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -80,7 +80,7 @@ $CONFIG = array( /** * Where user files are stored; this defaults to ``data/`` in the ownCloud - * directory. The SQLite database is also stored here, when you use SQLite. (SQLite is + * directory. The SQLite database is also stored here, when you use SQLite. (SQLite is * available only in ownCloud Community Edition) */ 'datadirectory' => '/var/www/owncloud/data', @@ -665,6 +665,20 @@ $CONFIG = array( 'OC\Preview\MarkDown' ), +/** + * LDAP + * + * Global settings used by LDAP User and Group Backend + */ + +/** + * defines the interval in minutes for the background job that checks user + * existance and marks them as ready to be cleaned up. The number is always + * minutes. Setting it to 0 disables the feature. + * See command line (occ) methods ldap:show-remnants and user:delete + */ +'ldapUserCleanupInterval' => 51, + /** * Maintenance diff --git a/core/command/user/delete.php b/core/command/user/delete.php new file mode 100644 index 00000000000..f64b40e4921 --- /dev/null +++ b/core/command/user/delete.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Core\Command\User; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; + +class Delete extends Command { + protected function configure() { + $this + ->setName('user:delete') + ->setDescription('deletes the specified user') + ->addArgument( + 'uid', + InputArgument::REQUIRED, + 'the username' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $wasSuccessful = \OC_User::deleteUser($input->getArgument('uid')); + if($wasSuccessful === true) { + $output->writeln('The specified user was deleted'); + return; + } + $output->writeln('<error>The specified could not be deleted. Please check the logs.</error>'); + } +} diff --git a/core/register_command.php b/core/register_command.php index 8f79473ced8..690e9879c47 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -22,5 +22,6 @@ $application->add(new OC\Core\Command\Maintenance\Repair($repair, \OC::$server-> $application->add(new OC\Core\Command\User\Report()); $application->add(new OC\Core\Command\User\ResetPassword(\OC::$server->getUserManager())); $application->add(new OC\Core\Command\User\LastSeen()); +$application->add(new OC\Core\Command\User\Delete()); $application->add(new OC\Core\Command\L10n\CreateJs()); diff --git a/lib/private/preferences.php b/lib/private/preferences.php index cd4a9fd1c19..1784d372261 100644 --- a/lib/private/preferences.php +++ b/lib/private/preferences.php @@ -137,10 +137,12 @@ class Preferences { * @param string $app * @param string $key * @param string $value + * @param int|null $limit + * @param int|null $offset * @return array * @deprecated use getUsersForUserValue of \OCP\IConfig instead */ - public function getUsersForValue($app, $key, $value) { + public function getUsersForValue($app, $key, $value, $limit = null, $offset = null) { return $this->config->getUsersForUserValue($app, $key, $value); } |