nextcloud/apps/user_ldap/lib/connection.php

623 lines
18 KiB
PHP
Raw Normal View History

<?php
/**
2015-03-26 13:44:34 +03:00
* @author Arthur Schiwon <blizzz@owncloud.com>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@owncloud.com>
* @author Lyonel Vincent <lyonel@ezix.org>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <icewind@owncloud.com>
2016-01-12 17:02:16 +03:00
* @author Robin McCorkell <robin@mccorkell.me.uk>
2015-03-26 13:44:34 +03:00
* @author Scrutinizer Auto-Fixer <auto-fixer@scrutinizer-ci.com>
*
2016-01-12 17:02:16 +03:00
* @copyright Copyright (c) 2016, ownCloud, Inc.
2015-03-26 13:44:34 +03:00
* @license AGPL-3.0
*
2015-03-26 13:44:34 +03:00
* 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.
*
2015-03-26 13:44:34 +03:00
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
2015-03-26 13:44:34 +03:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
2015-03-26 13:44:34 +03:00
* 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\lib;
2015-04-01 18:12:28 +03:00
use OC\ServerNotAvailableException;
/**
2015-04-09 15:03:30 +03:00
* magic properties (incomplete)
* responsible for LDAP connections in context with the provided configuration
2015-01-12 18:25:11 +03:00
*
* @property string ldapUserFilter
* @property string ldapUserDisplayName
* @property boolean hasPagedResultSupport
* @property string[] ldapBaseUsers
2015-01-12 18:25:11 +03:00
* @property int|string ldapPagingSize holds an integer
2015-01-29 02:15:55 +03:00
* @property bool|mixed|void ldapGroupMemberAssocAttr
2015-01-12 18:25:11 +03:00
*/
class Connection extends LDAPUtility {
private $ldapConnectionRes = null;
private $configPrefix;
private $configID;
private $configured = false;
//whether connection should be kept on __destruct
private $dontDestruct = false;
private $hasPagedResultSupport = true;
/**
* @var bool runtime flag that indicates whether supported primary groups are available
*/
public $hasPrimaryGroups = true;
//cache handler
protected $cache;
2015-04-02 16:05:37 +03:00
/** @var Configuration settings handler **/
protected $configuration;
protected $doNotValidate = false;
protected $ignoreValidation = false;
/**
* Constructor
* @param ILDAPWrapper $ldap
* @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, $configPrefix = '', $configID = 'user_ldap') {
parent::__construct($ldap);
$this->configPrefix = $configPrefix;
$this->configID = $configID;
$this->configuration = new Configuration($configPrefix,
!is_null($configID));
$memcache = \OC::$server->getMemCacheFactory();
2013-08-17 19:22:54 +04:00
if($memcache->isAvailable()) {
$this->cache = $memcache->create();
}
$this->hasPagedResultSupport =
2013-08-18 15:33:59 +04:00
$this->ldap->hasPagedResultSupport();
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
2014-08-21 19:59:13 +04:00
$helper = new Helper();
$this->doNotValidate = !in_array($this->configPrefix,
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
2014-08-21 19:59:13 +04:00
$helper->getServerConfigurationPrefixes());
}
public function __destruct() {
if(!$this->dontDestruct &&
$this->ldap->isResource($this->ldapConnectionRes)) {
2013-08-18 15:33:59 +04:00
@$this->ldap->unbind($this->ldapConnectionRes);
};
}
/**
* defines behaviour when the instance is cloned
*/
public function __clone() {
//a cloned instance inherits the connection resource. It may use it,
//but it may not disconnect it
$this->dontDestruct = true;
$this->configuration = new Configuration($this->configPrefix,
!is_null($this->configID));
}
/**
2014-05-13 15:29:25 +04:00
* @param string $name
* @return bool|mixed|void
*/
public function __get($name) {
if(!$this->configured) {
$this->readConfiguration();
}
if($name === 'hasPagedResultSupport') {
return $this->hasPagedResultSupport;
}
return $this->configuration->$name;
}
/**
2014-05-13 15:29:25 +04:00
* @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(!empty($this->configID)) {
$this->configuration->saveConfiguration();
}
$this->validateConfiguration();
}
}
/**
* 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();
}
/**
* Returns the LDAP handler
*/
public function getConnectionResource() {
if(!$this->ldapConnectionRes) {
$this->init();
} else if(!$this->ldap->isResource($this->ldapConnectionRes)) {
$this->ldapConnectionRes = null;
$this->establishConnection();
}
if(is_null($this->ldapConnectionRes)) {
2015-04-02 16:05:37 +03:00
\OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, \OCP\Util::ERROR);
2015-04-01 18:12:28 +03:00
throw new ServerNotAvailableException('Connection to LDAP server could not be established');
}
return $this->ldapConnectionRes;
}
/**
2014-05-13 15:29:25 +04:00
* @param string|null $key
* @return string
*/
private function getCacheKey($key) {
$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
if(is_null($key)) {
return $prefix;
}
return $prefix.md5($key);
}
/**
2014-05-13 15:29:25 +04:00
* @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;
}
if(!$this->isCached($key)) {
return null;
}
$key = $this->getCacheKey($key);
return json_decode(base64_decode($this->cache->get($key)), true);
}
/**
2014-05-13 15:29:25 +04:00
* @param string $key
* @return bool
*/
public function isCached($key) {
if(!$this->configured) {
$this->readConfiguration();
}
if(is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
return false;
}
$key = $this->getCacheKey($key);
return $this->cache->hasKey($key);
}
/**
2014-05-13 15:29:25 +04:00
* @param string $key
* @param mixed $value
*
* @return string
*/
public function writeToCache($key, $value) {
if(!$this->configured) {
$this->readConfiguration();
}
if(is_null($this->cache)
|| !$this->configuration->ldapCacheTTL
|| !$this->configuration->ldapConfigurationActive) {
return null;
}
$key = $this->getCacheKey($key);
$value = base64_encode(json_encode($value));
$this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
}
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();
}
}
2013-01-30 06:44:11 +04:00
/**
* set LDAP configuration with values delivered by an array, not read from configuration
2014-05-13 15:29:25 +04:00
* @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 boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
*/
public function setConfiguration($config, &$setParameters = null) {
if(is_null($setParameters)) {
$setParameters = array();
}
$this->doNotValidate = false;
$this->configuration->setConfiguration($config, $setParameters);
if(count($setParameters) > 0) {
$this->configured = $this->validateConfiguration();
}
return $this->configured;
}
2013-01-30 06:44:11 +04:00
/**
* saves the current Configuration in the database and empties the
* cache
* @return null
2013-01-30 06:44:11 +04:00
*/
2013-01-20 21:02:44 +04:00
public function saveConfiguration() {
$this->configuration->saveConfiguration();
2013-01-24 15:44:30 +04:00
$this->clearCache();
2013-01-20 21:02:44 +04:00
}
2013-01-18 16:53:26 +04:00
/**
* get the current LDAP configuration
2013-01-18 16:53:26 +04:00
* @return array
*/
public function getConfiguration() {
$this->readConfiguration();
$config = $this->configuration->getConfiguration();
$cta = $this->configuration->getConfigTranslationArray();
$result = array();
foreach($cta as $dbkey => $configkey) {
switch($configkey) {
case 'homeFolderNamingRule':
if(strpos($config[$configkey], 'attr:') === 0) {
$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
default:
$result[$dbkey] = $config[$configkey];
}
2013-01-20 21:02:44 +04:00
}
return $result;
2013-01-18 16:53:26 +04:00
}
private function doSoftValidation() {
//if User or Group Base are not set, take over Base DN setting
foreach(array('ldapBaseUsers', 'ldapBaseGroups') as $keyBase) {
2014-05-16 00:47:28 +04:00
$val = $this->configuration->$keyBase;
if(empty($val)) {
$obj = strpos('Users', $keyBase) !== false ? 'Users' : 'Groups';
\OCP\Util::writeLog('user_ldap',
'Base tree for '.$obj.
' is empty, using Base DN',
\OCP\Util::INFO);
$this->configuration->$keyBase = $this->configuration->ldapBase;
}
}
2013-10-17 21:40:59 +04:00
foreach(array('ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute')
as $expertSetting => $effectiveSetting) {
$uuidOverride = $this->configuration->$expertSetting;
if(!empty($uuidOverride)) {
$this->configuration->$effectiveSetting = $uuidOverride;
} else {
$uuidAttributes = array('auto', 'entryuuid', 'nsuniqueid',
'objectguid', 'guid');
if(!in_array($this->configuration->$effectiveSetting,
$uuidAttributes)
&& (!is_null($this->configID))) {
$this->configuration->$effectiveSetting = 'auto';
$this->configuration->saveConfiguration();
\OCP\Util::writeLog('user_ldap',
'Illegal value for the '.
$effectiveSetting.', '.'reset to '.
'autodetect.', \OCP\Util::INFO);
}
2013-10-17 21:40:59 +04:00
}
}
$backupPort = $this->configuration->ldapBackupPort;
if(empty($backupPort)) {
$this->configuration->backupPort = $this->configuration->ldapPort;
}
//make sure empty search attributes are saved as simple, empty array
$saKeys = array('ldapAttributesForUserSearch',
'ldapAttributesForGroupSearch');
foreach($saKeys as $key) {
$val = $this->configuration->$key;
if(is_array($val) && count($val) === 1 && empty($val[0])) {
$this->configuration->$key = array();
}
}
if((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
&& $this->configuration->ldapTLS) {
$this->configuration->ldapTLS = false;
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
'LDAPS (already using secure connection) and '.
'TLS do not work together. Switched off TLS.',
\OCP\Util::INFO);
}
}
/**
* @return bool
*/
private function doCriticalValidation() {
$configurationOK = true;
$errorStr = 'Configuration Error (prefix '.
strval($this->configPrefix).'): ';
//options that shall not be empty
$options = array('ldapHost', 'ldapPort', 'ldapUserDisplayName',
'ldapGroupDisplayName', 'ldapLoginFilter');
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;
\OCP\Util::writeLog('user_ldap',
$errorStr.'No '.$subj.' given!',
\OCP\Util::WARN);
}
}
//combinations
$agent = $this->configuration->ldapAgentName;
$pwd = $this->configuration->ldapAgentPassword;
if((empty($agent) && !empty($pwd)) || (!empty($agent) && empty($pwd))) {
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
$errorStr.'either no password is given for the'.
'user agent or a password is given, but not an'.
'LDAP agent.',
2013-02-15 01:16:48 +04:00
\OCP\Util::WARN);
$configurationOK = false;
}
$base = $this->configuration->ldapBase;
$baseUsers = $this->configuration->ldapBaseUsers;
$baseGroups = $this->configuration->ldapBaseGroups;
if(empty($base) && empty($baseUsers) && empty($baseGroups)) {
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
$errorStr.'Not a single Base DN given.',
\OCP\Util::WARN);
$configurationOK = false;
}
if(mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
=== false) {
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
$errorStr.'login filter does not contain %uid '.
'place holder.',
\OCP\Util::WARN);
$configurationOK = false;
}
return $configurationOK;
}
/**
* Validates the user specified configuration
2014-05-11 18:29:59 +04:00
* @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
*/
private function establishConnection() {
if(!$this->configuration->ldapConfigurationActive) {
return null;
}
static $phpLDAPinstalled = true;
if(!$phpLDAPinstalled) {
return false;
}
if(!$this->ignoreValidation && !$this->configured) {
\OCP\Util::writeLog('user_ldap',
'Configuration is invalid, cannot connect',
\OCP\Util::WARN);
return false;
}
if(!$this->ldapConnectionRes) {
2013-08-18 15:33:59 +04:00
if(!$this->ldap->areLDAPFunctionsAvailable()) {
$phpLDAPinstalled = false;
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
'function ldap_connect is not available. Make '.
'sure that the PHP ldap module is installed.',
\OCP\Util::ERROR);
return false;
}
if($this->configuration->turnOffCertCheck) {
if(putenv('LDAPTLS_REQCERT=never')) {
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
'Turned off SSL certificate validation successfully.',
\OCP\Util::DEBUG);
} else {
\OCP\Util::writeLog('user_ldap',
'Could not turn off SSL certificate validation.',
\OCP\Util::WARN);
}
}
$bindStatus = false;
$error = null;
try {
if (!$this->configuration->ldapOverrideMainServer
&& !$this->getFromCache('overrideMainServer')
) {
$this->doConnect($this->configuration->ldapHost,
$this->configuration->ldapPort);
$bindStatus = $this->bind();
$error = $this->ldap->isResource($this->ldapConnectionRes) ?
$this->ldap->errno($this->ldapConnectionRes) : -1;
}
if($bindStatus === true) {
return $bindStatus;
}
} catch (\OC\ServerNotAvailableException $e) {
if(trim($this->configuration->ldapBackupHost) === "") {
throw $e;
}
}
//if LDAP server is not reachable, try the Backup (Replica!) Server
if( $error !== 0
|| $this->configuration->ldapOverrideMainServer
|| $this->getFromCache('overrideMainServer'))
{
$this->doConnect($this->configuration->ldapBackupHost,
$this->configuration->ldapBackupPort);
$bindStatus = $this->bind();
if($bindStatus && $error === -1) {
//when bind to backup server succeeded and failed to main server,
//skip contacting him until next cache refresh
$this->writeToCache('overrideMainServer', true);
}
}
return $bindStatus;
}
}
/**
2014-05-13 15:29:25 +04:00
* @param string $host
* @param string $port
* @return false|void
* @throws \OC\ServerNotAvailableException
*/
private function doConnect($host, $port) {
if(empty($host)) {
return false;
}
2013-08-18 15:33:59 +04:00
$this->ldapConnectionRes = $this->ldap->connect($host, $port);
if($this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
if($this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
if($this->configuration->ldapTLS) {
$this->ldap->startTls($this->ldapConnectionRes);
}
}
} else {
throw new \OC\ServerNotAvailableException('Could not set required LDAP Protocol version.');
}
}
/**
* Binds to LDAP
*/
public function bind() {
2013-05-25 13:03:58 +04:00
static $getConnectionResourceAttempt = false;
if(!$this->configuration->ldapConfigurationActive) {
return false;
}
2013-05-25 13:03:58 +04:00
if($getConnectionResourceAttempt) {
$getConnectionResourceAttempt = false;
return false;
}
$getConnectionResourceAttempt = true;
$cr = $this->getConnectionResource();
2013-05-25 13:03:58 +04:00
$getConnectionResourceAttempt = false;
if(!$this->ldap->isResource($cr)) {
return false;
}
2013-09-11 21:42:08 +04:00
$ldapLogin = @$this->ldap->bind($cr,
$this->configuration->ldapAgentName,
$this->configuration->ldapAgentPassword);
if(!$ldapLogin) {
2013-02-15 01:16:48 +04:00
\OCP\Util::writeLog('user_ldap',
2013-08-18 15:33:59 +04:00
'Bind failed: ' . $this->ldap->errno($cr) . ': ' . $this->ldap->error($cr),
\OCP\Util::WARN);
$this->ldapConnectionRes = null;
return false;
}
return true;
}
}