1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 6 * @author Bart Visscher <bartv@thisnet.nl> 7 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 8 * @author Jarkko Lehtoranta <devel@jlranta.com> 9 * @author Joas Schilling <coding@schilljs.com> 10 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 11 * @author Julius Härtl <jus@bitgrid.net> 12 * @author Lukas Reschke <lukas@statuscode.ch> 13 * @author Morris Jobke <hey@morrisjobke.de> 14 * @author Robin Appelman <robin@icewind.nl> 15 * @author Robin McCorkell <robin@mccorkell.me.uk> 16 * @author Roeland Jago Douma <roeland@famdouma.nl> 17 * @author Roger Szabo <roger.szabo@web.de> 18 * @author root <root@localhost.localdomain> 19 * @author Victor Dubiniuk <dubiniuk@owncloud.com> 20 * @author Xuanwo <xuanwo@yunify.com> 21 * 22 * @license AGPL-3.0 23 * 24 * This code is free software: you can redistribute it and/or modify 25 * it under the terms of the GNU Affero General Public License, version 3, 26 * as published by the Free Software Foundation. 27 * 28 * This program is distributed in the hope that it will be useful, 29 * but WITHOUT ANY WARRANTY; without even the implied warranty of 30 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31 * GNU Affero General Public License for more details. 32 * 33 * You should have received a copy of the GNU Affero General Public License, version 3, 34 * along with this program. If not, see <http://www.gnu.org/licenses/> 35 * 36 */ 37namespace OCA\User_LDAP; 38 39use OC\ServerNotAvailableException; 40use Psr\Log\LoggerInterface; 41 42/** 43 * magic properties (incomplete) 44 * responsible for LDAP connections in context with the provided configuration 45 * 46 * @property string ldapHost 47 * @property string ldapPort holds the port number 48 * @property string ldapUserFilter 49 * @property string ldapUserDisplayName 50 * @property string ldapUserDisplayName2 51 * @property string ldapUserAvatarRule 52 * @property boolean turnOnPasswordChange 53 * @property string[] ldapBaseUsers 54 * @property int|null ldapPagingSize holds an integer 55 * @property bool|mixed|void ldapGroupMemberAssocAttr 56 * @property string ldapUuidUserAttribute 57 * @property string ldapUuidGroupAttribute 58 * @property string ldapExpertUUIDUserAttr 59 * @property string ldapExpertUUIDGroupAttr 60 * @property string ldapQuotaAttribute 61 * @property string ldapQuotaDefault 62 * @property string ldapEmailAttribute 63 * @property string ldapExtStorageHomeAttribute 64 * @property string homeFolderNamingRule 65 * @property bool|string ldapNestedGroups 66 * @property string[] ldapBaseGroups 67 * @property string ldapGroupFilter 68 * @property string ldapGroupDisplayName 69 * @property string ldapLoginFilter 70 * @property string ldapDynamicGroupMemberURL 71 * @property string ldapGidNumber 72 * @property int hasMemberOfFilterSupport 73 * @property int useMemberOfToDetectMembership 74 * @property string ldapMatchingRuleInChainState 75 */ 76class Connection extends LDAPUtility { 77 private $ldapConnectionRes = null; 78 private $configPrefix; 79 private $configID; 80 private $configured = false; 81 //whether connection should be kept on __destruct 82 private $dontDestruct = false; 83 84 /** 85 * @var bool runtime flag that indicates whether supported primary groups are available 86 */ 87 public $hasPrimaryGroups = true; 88 89 /** 90 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available 91 */ 92 public $hasGidNumber = true; 93 94 //cache handler 95 protected $cache; 96 97 /** @var Configuration settings handler **/ 98 protected $configuration; 99 100 protected $doNotValidate = false; 101 102 protected $ignoreValidation = false; 103 104 protected $bindResult = []; 105 106 /** @var LoggerInterface */ 107 protected $logger; 108 109 /** 110 * Constructor 111 * @param ILDAPWrapper $ldap 112 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table) 113 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections 114 */ 115 public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') { 116 parent::__construct($ldap); 117 $this->configPrefix = $configPrefix; 118 $this->configID = $configID; 119 $this->configuration = new Configuration($configPrefix, 120 !is_null($configID)); 121 $memcache = \OC::$server->getMemCacheFactory(); 122 if ($memcache->isAvailable()) { 123 $this->cache = $memcache->createDistributed(); 124 } 125 $helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); 126 $this->doNotValidate = !in_array($this->configPrefix, 127 $helper->getServerConfigurationPrefixes()); 128 $this->logger = \OC::$server->get(LoggerInterface::class); 129 } 130 131 public function __destruct() { 132 if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) { 133 @$this->ldap->unbind($this->ldapConnectionRes); 134 $this->bindResult = []; 135 } 136 } 137 138 /** 139 * defines behaviour when the instance is cloned 140 */ 141 public function __clone() { 142 $this->configuration = new Configuration($this->configPrefix, 143 !is_null($this->configID)); 144 if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) { 145 $this->bindResult = []; 146 } 147 $this->ldapConnectionRes = null; 148 $this->dontDestruct = true; 149 } 150 151 public function __get(string $name) { 152 if (!$this->configured) { 153 $this->readConfiguration(); 154 } 155 156 return $this->configuration->$name; 157 } 158 159 /** 160 * @param string $name 161 * @param mixed $value 162 */ 163 public function __set($name, $value) { 164 $this->doNotValidate = false; 165 $before = $this->configuration->$name; 166 $this->configuration->$name = $value; 167 $after = $this->configuration->$name; 168 if ($before !== $after) { 169 if ($this->configID !== '' && $this->configID !== null) { 170 $this->configuration->saveConfiguration(); 171 } 172 $this->validateConfiguration(); 173 } 174 } 175 176 /** 177 * @param string $rule 178 * @return array 179 * @throws \RuntimeException 180 */ 181 public function resolveRule($rule) { 182 return $this->configuration->resolveRule($rule); 183 } 184 185 /** 186 * sets whether the result of the configuration validation shall 187 * be ignored when establishing the connection. Used by the Wizard 188 * in early configuration state. 189 * @param bool $state 190 */ 191 public function setIgnoreValidation($state) { 192 $this->ignoreValidation = (bool)$state; 193 } 194 195 /** 196 * initializes the LDAP backend 197 * @param bool $force read the config settings no matter what 198 */ 199 public function init($force = false) { 200 $this->readConfiguration($force); 201 $this->establishConnection(); 202 } 203 204 /** 205 * Returns the LDAP handler 206 */ 207 public function getConnectionResource() { 208 if (!$this->ldapConnectionRes) { 209 $this->init(); 210 } elseif (!$this->ldap->isResource($this->ldapConnectionRes)) { 211 $this->ldapConnectionRes = null; 212 $this->establishConnection(); 213 } 214 if (is_null($this->ldapConnectionRes)) { 215 $this->logger->error( 216 'No LDAP Connection to server ' . $this->configuration->ldapHost, 217 ['app' => 'user_ldap'] 218 ); 219 throw new ServerNotAvailableException('Connection to LDAP server could not be established'); 220 } 221 return $this->ldapConnectionRes; 222 } 223 224 /** 225 * resets the connection resource 226 */ 227 public function resetConnectionResource() { 228 if (!is_null($this->ldapConnectionRes)) { 229 @$this->ldap->unbind($this->ldapConnectionRes); 230 $this->ldapConnectionRes = null; 231 $this->bindResult = []; 232 } 233 } 234 235 /** 236 * @param string|null $key 237 * @return string 238 */ 239 private function getCacheKey($key) { 240 $prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-'; 241 if (is_null($key)) { 242 return $prefix; 243 } 244 return $prefix.hash('sha256', $key); 245 } 246 247 /** 248 * @param string $key 249 * @return mixed|null 250 */ 251 public function getFromCache($key) { 252 if (!$this->configured) { 253 $this->readConfiguration(); 254 } 255 if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) { 256 return null; 257 } 258 $key = $this->getCacheKey($key); 259 260 return json_decode(base64_decode($this->cache->get($key)), true); 261 } 262 263 /** 264 * @param string $key 265 * @param mixed $value 266 */ 267 public function writeToCache($key, $value): void { 268 if (!$this->configured) { 269 $this->readConfiguration(); 270 } 271 if (is_null($this->cache) 272 || !$this->configuration->ldapCacheTTL 273 || !$this->configuration->ldapConfigurationActive) { 274 return; 275 } 276 $key = $this->getCacheKey($key); 277 $value = base64_encode(json_encode($value)); 278 $this->cache->set($key, $value, $this->configuration->ldapCacheTTL); 279 } 280 281 public function clearCache() { 282 if (!is_null($this->cache)) { 283 $this->cache->clear($this->getCacheKey(null)); 284 } 285 } 286 287 /** 288 * Caches the general LDAP configuration. 289 * @param bool $force optional. true, if the re-read should be forced. defaults 290 * to false. 291 * @return null 292 */ 293 private function readConfiguration($force = false) { 294 if ((!$this->configured || $force) && !is_null($this->configID)) { 295 $this->configuration->readConfiguration(); 296 $this->configured = $this->validateConfiguration(); 297 } 298 } 299 300 /** 301 * set LDAP configuration with values delivered by an array, not read from configuration 302 * @param array $config array that holds the config parameters in an associated array 303 * @param array &$setParameters optional; array where the set fields will be given to 304 * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters 305 */ 306 public function setConfiguration($config, &$setParameters = null) { 307 if (is_null($setParameters)) { 308 $setParameters = []; 309 } 310 $this->doNotValidate = false; 311 $this->configuration->setConfiguration($config, $setParameters); 312 if (count($setParameters) > 0) { 313 $this->configured = $this->validateConfiguration(); 314 } 315 316 317 return $this->configured; 318 } 319 320 /** 321 * saves the current Configuration in the database and empties the 322 * cache 323 * @return null 324 */ 325 public function saveConfiguration() { 326 $this->configuration->saveConfiguration(); 327 $this->clearCache(); 328 } 329 330 /** 331 * get the current LDAP configuration 332 * @return array 333 */ 334 public function getConfiguration() { 335 $this->readConfiguration(); 336 $config = $this->configuration->getConfiguration(); 337 $cta = $this->configuration->getConfigTranslationArray(); 338 $result = []; 339 foreach ($cta as $dbkey => $configkey) { 340 switch ($configkey) { 341 case 'homeFolderNamingRule': 342 if (strpos($config[$configkey], 'attr:') === 0) { 343 $result[$dbkey] = substr($config[$configkey], 5); 344 } else { 345 $result[$dbkey] = ''; 346 } 347 break; 348 case 'ldapBase': 349 case 'ldapBaseUsers': 350 case 'ldapBaseGroups': 351 case 'ldapAttributesForUserSearch': 352 case 'ldapAttributesForGroupSearch': 353 if (is_array($config[$configkey])) { 354 $result[$dbkey] = implode("\n", $config[$configkey]); 355 break; 356 } //else follows default 357 // no break 358 default: 359 $result[$dbkey] = $config[$configkey]; 360 } 361 } 362 return $result; 363 } 364 365 private function doSoftValidation() { 366 //if User or Group Base are not set, take over Base DN setting 367 foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) { 368 $val = $this->configuration->$keyBase; 369 if (empty($val)) { 370 $this->configuration->$keyBase = $this->configuration->ldapBase; 371 } 372 } 373 374 foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute', 375 'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] 376 as $expertSetting => $effectiveSetting) { 377 $uuidOverride = $this->configuration->$expertSetting; 378 if (!empty($uuidOverride)) { 379 $this->configuration->$effectiveSetting = $uuidOverride; 380 } else { 381 $uuidAttributes = Access::UUID_ATTRIBUTES; 382 array_unshift($uuidAttributes, 'auto'); 383 if (!in_array($this->configuration->$effectiveSetting, 384 $uuidAttributes) 385 && (!is_null($this->configID))) { 386 $this->configuration->$effectiveSetting = 'auto'; 387 $this->configuration->saveConfiguration(); 388 $this->logger->info( 389 'Illegal value for the '.$effectiveSetting.', reset to autodetect.', 390 ['app' => 'user_ldap'] 391 ); 392 } 393 } 394 } 395 396 $backupPort = (int)$this->configuration->ldapBackupPort; 397 if ($backupPort <= 0) { 398 $this->configuration->backupPort = $this->configuration->ldapPort; 399 } 400 401 //make sure empty search attributes are saved as simple, empty array 402 $saKeys = ['ldapAttributesForUserSearch', 403 'ldapAttributesForGroupSearch']; 404 foreach ($saKeys as $key) { 405 $val = $this->configuration->$key; 406 if (is_array($val) && count($val) === 1 && empty($val[0])) { 407 $this->configuration->$key = []; 408 } 409 } 410 411 if ((stripos($this->configuration->ldapHost, 'ldaps://') === 0) 412 && $this->configuration->ldapTLS) { 413 $this->configuration->ldapTLS = false; 414 $this->logger->info( 415 'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.', 416 ['app' => 'user_ldap'] 417 ); 418 } 419 } 420 421 /** 422 * @return bool 423 */ 424 private function doCriticalValidation() { 425 $configurationOK = true; 426 $errorStr = 'Configuration Error (prefix '. 427 (string)$this->configPrefix .'): '; 428 429 //options that shall not be empty 430 $options = ['ldapHost', 'ldapPort', 'ldapUserDisplayName', 431 'ldapGroupDisplayName', 'ldapLoginFilter']; 432 foreach ($options as $key) { 433 $val = $this->configuration->$key; 434 if (empty($val)) { 435 switch ($key) { 436 case 'ldapHost': 437 $subj = 'LDAP Host'; 438 break; 439 case 'ldapPort': 440 $subj = 'LDAP Port'; 441 break; 442 case 'ldapUserDisplayName': 443 $subj = 'LDAP User Display Name'; 444 break; 445 case 'ldapGroupDisplayName': 446 $subj = 'LDAP Group Display Name'; 447 break; 448 case 'ldapLoginFilter': 449 $subj = 'LDAP Login Filter'; 450 break; 451 default: 452 $subj = $key; 453 break; 454 } 455 $configurationOK = false; 456 $this->logger->warning( 457 $errorStr.'No '.$subj.' given!', 458 ['app' => 'user_ldap'] 459 ); 460 } 461 } 462 463 //combinations 464 $agent = $this->configuration->ldapAgentName; 465 $pwd = $this->configuration->ldapAgentPassword; 466 if ( 467 ($agent === '' && $pwd !== '') 468 || ($agent !== '' && $pwd === '') 469 ) { 470 $this->logger->warning( 471 $errorStr.'either no password is given for the user ' . 472 'agent or a password is given, but not an LDAP agent.', 473 ['app' => 'user_ldap'] 474 ); 475 $configurationOK = false; 476 } 477 478 $base = $this->configuration->ldapBase; 479 $baseUsers = $this->configuration->ldapBaseUsers; 480 $baseGroups = $this->configuration->ldapBaseGroups; 481 482 if (empty($base) && empty($baseUsers) && empty($baseGroups)) { 483 $this->logger->warning( 484 $errorStr.'Not a single Base DN given.', 485 ['app' => 'user_ldap'] 486 ); 487 $configurationOK = false; 488 } 489 490 if (mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') 491 === false) { 492 $this->logger->warning( 493 $errorStr.'login filter does not contain %uid place holder.', 494 ['app' => 'user_ldap'] 495 ); 496 $configurationOK = false; 497 } 498 499 return $configurationOK; 500 } 501 502 /** 503 * Validates the user specified configuration 504 * @return bool true if configuration seems OK, false otherwise 505 */ 506 private function validateConfiguration() { 507 if ($this->doNotValidate) { 508 //don't do a validation if it is a new configuration with pure 509 //default values. Will be allowed on changes via __set or 510 //setConfiguration 511 return false; 512 } 513 514 // first step: "soft" checks: settings that are not really 515 // necessary, but advisable. If left empty, give an info message 516 $this->doSoftValidation(); 517 518 //second step: critical checks. If left empty or filled wrong, mark as 519 //not configured and give a warning. 520 return $this->doCriticalValidation(); 521 } 522 523 524 /** 525 * Connects and Binds to LDAP 526 * 527 * @throws ServerNotAvailableException 528 */ 529 private function establishConnection() { 530 if (!$this->configuration->ldapConfigurationActive) { 531 return null; 532 } 533 static $phpLDAPinstalled = true; 534 if (!$phpLDAPinstalled) { 535 return false; 536 } 537 if (!$this->ignoreValidation && !$this->configured) { 538 $this->logger->warning( 539 'Configuration is invalid, cannot connect', 540 ['app' => 'user_ldap'] 541 ); 542 return false; 543 } 544 if (!$this->ldapConnectionRes) { 545 if (!$this->ldap->areLDAPFunctionsAvailable()) { 546 $phpLDAPinstalled = false; 547 $this->logger->error( 548 'function ldap_connect is not available. Make sure that the PHP ldap module is installed.', 549 ['app' => 'user_ldap'] 550 ); 551 552 return false; 553 } 554 if ($this->configuration->turnOffCertCheck) { 555 if (putenv('LDAPTLS_REQCERT=never')) { 556 $this->logger->debug( 557 'Turned off SSL certificate validation successfully.', 558 ['app' => 'user_ldap'] 559 ); 560 } else { 561 $this->logger->warning( 562 'Could not turn off SSL certificate validation.', 563 ['app' => 'user_ldap'] 564 ); 565 } 566 } 567 568 $isOverrideMainServer = ($this->configuration->ldapOverrideMainServer 569 || $this->getFromCache('overrideMainServer')); 570 $isBackupHost = (trim($this->configuration->ldapBackupHost) !== ""); 571 $bindStatus = false; 572 try { 573 if (!$isOverrideMainServer) { 574 $this->doConnect($this->configuration->ldapHost, 575 $this->configuration->ldapPort); 576 return $this->bind(); 577 } 578 } catch (ServerNotAvailableException $e) { 579 if (!$isBackupHost) { 580 throw $e; 581 } 582 } 583 584 //if LDAP server is not reachable, try the Backup (Replica!) Server 585 if ($isBackupHost || $isOverrideMainServer) { 586 $this->doConnect($this->configuration->ldapBackupHost, 587 $this->configuration->ldapBackupPort); 588 $this->bindResult = []; 589 $bindStatus = $this->bind(); 590 $error = $this->ldap->isResource($this->ldapConnectionRes) ? 591 $this->ldap->errno($this->ldapConnectionRes) : -1; 592 if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) { 593 //when bind to backup server succeeded and failed to main server, 594 //skip contacting him until next cache refresh 595 $this->writeToCache('overrideMainServer', true); 596 } 597 } 598 599 return $bindStatus; 600 } 601 return null; 602 } 603 604 /** 605 * @param string $host 606 * @param string $port 607 * @return bool 608 * @throws \OC\ServerNotAvailableException 609 */ 610 private function doConnect($host, $port) { 611 if ($host === '') { 612 return false; 613 } 614 615 $this->ldapConnectionRes = $this->ldap->connect($host, $port); 616 617 if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) { 618 throw new ServerNotAvailableException('Could not set required LDAP Protocol version.'); 619 } 620 621 if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) { 622 throw new ServerNotAvailableException('Could not disable LDAP referrals.'); 623 } 624 625 if ($this->configuration->ldapTLS) { 626 if (!$this->ldap->startTls($this->ldapConnectionRes)) { 627 throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.'); 628 } 629 } 630 631 return true; 632 } 633 634 /** 635 * Binds to LDAP 636 */ 637 public function bind() { 638 if (!$this->configuration->ldapConfigurationActive) { 639 return false; 640 } 641 $cr = $this->ldapConnectionRes; 642 if (!$this->ldap->isResource($cr)) { 643 $cr = $this->getConnectionResource(); 644 } 645 646 if ( 647 count($this->bindResult) !== 0 648 && $this->bindResult['dn'] === $this->configuration->ldapAgentName 649 && \OC::$server->getHasher()->verify( 650 $this->configPrefix . $this->configuration->ldapAgentPassword, 651 $this->bindResult['hash'] 652 ) 653 ) { 654 // don't attempt to bind again with the same data as before 655 // bind might have been invoked via getConnectionResource(), 656 // but we need results specifically for e.g. user login 657 return $this->bindResult['result']; 658 } 659 660 $ldapLogin = @$this->ldap->bind($cr, 661 $this->configuration->ldapAgentName, 662 $this->configuration->ldapAgentPassword); 663 664 $this->bindResult = [ 665 'dn' => $this->configuration->ldapAgentName, 666 'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword), 667 'result' => $ldapLogin, 668 ]; 669 670 if (!$ldapLogin) { 671 $errno = $this->ldap->errno($cr); 672 673 $this->logger->warning( 674 'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr), 675 ['app' => 'user_ldap'] 676 ); 677 678 // Set to failure mode, if LDAP error code is not one of 679 // - LDAP_SUCCESS (0) 680 // - LDAP_INVALID_CREDENTIALS (49) 681 // - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory) 682 // - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory) 683 if (!in_array($errno, [0, 49, 50, 53], true)) { 684 $this->ldapConnectionRes = null; 685 } 686 687 return false; 688 } 689 return true; 690 } 691} 692