PageRenderTime 69ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/user_ldap/lib/access.php

https://github.com/sezuan/core
PHP | 1124 lines | 730 code | 102 blank | 292 comment | 117 complexity | f10bc74d04f1ed58df6f686005496b0f MD5 | raw file
Possible License(s): AGPL-3.0, AGPL-1.0, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * ownCloud – LDAP Access
  4. *
  5. * @author Arthur Schiwon
  6. * @copyright 2012, 2013 Arthur Schiwon blizzz@owncloud.com
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OCA\user_ldap\lib;
  23. abstract class Access {
  24. protected $connection;
  25. //never ever check this var directly, always use getPagedSearchResultState
  26. protected $pagedSearchedSuccessful;
  27. public function setConnector(Connection &$connection) {
  28. $this->connection = $connection;
  29. }
  30. private function checkConnection() {
  31. return ($this->connection instanceof Connection);
  32. }
  33. /**
  34. * @brief reads a given attribute for an LDAP record identified by a DN
  35. * @param $dn the record in question
  36. * @param $attr the attribute that shall be retrieved
  37. * if empty, just check the record's existence
  38. * @returns an array of values on success or an empty
  39. * array if $attr is empty, false otherwise
  40. *
  41. * Reads an attribute from an LDAP entry or check if entry exists
  42. */
  43. public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
  44. if(!$this->checkConnection()) {
  45. \OCP\Util::writeLog('user_ldap',
  46. 'No LDAP Connector assigned, access impossible for readAttribute.',
  47. \OCP\Util::WARN);
  48. return false;
  49. }
  50. $cr = $this->connection->getConnectionResource();
  51. if(!is_resource($cr)) {
  52. //LDAP not available
  53. \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
  54. return false;
  55. }
  56. $dn = $this->DNasBaseParameter($dn);
  57. $rr = @ldap_read($cr, $dn, $filter, array($attr));
  58. if(!is_resource($rr)) {
  59. if(!empty($attr)) {
  60. //do not throw this message on userExists check, irritates
  61. \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
  62. }
  63. //in case an error occurs , e.g. object does not exist
  64. return false;
  65. }
  66. if (empty($attr)) {
  67. \OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
  68. return array();
  69. }
  70. $er = ldap_first_entry($cr, $rr);
  71. if(!is_resource($er)) {
  72. //did not match the filter, return false
  73. return false;
  74. }
  75. //LDAP attributes are not case sensitive
  76. $result = \OCP\Util::mb_array_change_key_case(ldap_get_attributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
  77. $attr = mb_strtolower($attr, 'UTF-8');
  78. if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
  79. $values = array();
  80. for($i=0;$i<$result[$attr]['count'];$i++) {
  81. if($this->resemblesDN($attr)) {
  82. $values[] = $this->sanitizeDN($result[$attr][$i]);
  83. } elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
  84. $values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
  85. } else {
  86. $values[] = $result[$attr][$i];
  87. }
  88. }
  89. return $values;
  90. }
  91. \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
  92. return false;
  93. }
  94. /**
  95. * @brief checks wether the given attribute`s valua is probably a DN
  96. * @param $attr the attribute in question
  97. * @return if so true, otherwise false
  98. */
  99. private function resemblesDN($attr) {
  100. $resemblingAttributes = array(
  101. 'dn',
  102. 'uniquemember',
  103. 'member'
  104. );
  105. return in_array($attr, $resemblingAttributes);
  106. }
  107. /**
  108. * @brief sanitizes a DN received from the LDAP server
  109. * @param $dn the DN in question
  110. * @return the sanitized DN
  111. */
  112. private function sanitizeDN($dn) {
  113. //treating multiple base DNs
  114. if(is_array($dn)) {
  115. $result = array();
  116. foreach($dn as $singleDN) {
  117. $result[] = $this->sanitizeDN($singleDN);
  118. }
  119. return $result;
  120. }
  121. //OID sometimes gives back DNs with whitespace after the comma
  122. // a la "uid=foo, cn=bar, dn=..." We need to tackle this!
  123. $dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
  124. //make comparisons and everything work
  125. $dn = mb_strtolower($dn, 'UTF-8');
  126. //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
  127. //to use the DN in search filters, \ needs to be escaped to \5c additionally
  128. //to use them in bases, we convert them back to simple backslashes in readAttribute()
  129. $replacements = array(
  130. '\,' => '\5c2C',
  131. '\=' => '\5c3D',
  132. '\+' => '\5c2B',
  133. '\<' => '\5c3C',
  134. '\>' => '\5c3E',
  135. '\;' => '\5c3B',
  136. '\"' => '\5c22',
  137. '\#' => '\5c23',
  138. '(' => '\28',
  139. ')' => '\29',
  140. '*' => '\2A',
  141. );
  142. $dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
  143. return $dn;
  144. }
  145. /**
  146. * gives back the database table for the query
  147. */
  148. private function getMapTable($isUser) {
  149. if($isUser) {
  150. return '*PREFIX*ldap_user_mapping';
  151. } else {
  152. return '*PREFIX*ldap_group_mapping';
  153. }
  154. }
  155. /**
  156. * @brief returns the LDAP DN for the given internal ownCloud name of the group
  157. * @param $name the ownCloud name in question
  158. * @returns string with the LDAP DN on success, otherwise false
  159. *
  160. * returns the LDAP DN for the given internal ownCloud name of the group
  161. */
  162. public function groupname2dn($name) {
  163. $dn = $this->ocname2dn($name, false);
  164. if($dn) {
  165. return $dn;
  166. }
  167. return false;
  168. }
  169. /**
  170. * @brief returns the LDAP DN for the given internal ownCloud name of the user
  171. * @param $name the ownCloud name in question
  172. * @returns string with the LDAP DN on success, otherwise false
  173. *
  174. * returns the LDAP DN for the given internal ownCloud name of the user
  175. */
  176. public function username2dn($name) {
  177. $dn = $this->ocname2dn($name, true);
  178. if($dn) {
  179. return $dn;
  180. }
  181. return false;
  182. }
  183. /**
  184. * @brief returns the LDAP DN for the given internal ownCloud name
  185. * @param $name the ownCloud name in question
  186. * @param $isUser is it a user? otherwise group
  187. * @returns string with the LDAP DN on success, otherwise false
  188. *
  189. * returns the LDAP DN for the given internal ownCloud name
  190. */
  191. private function ocname2dn($name, $isUser) {
  192. $table = $this->getMapTable($isUser);
  193. $query = \OCP\DB::prepare('
  194. SELECT `ldap_dn`
  195. FROM `'.$table.'`
  196. WHERE `owncloud_name` = ?
  197. ');
  198. $record = $query->execute(array($name))->fetchOne();
  199. return $record;
  200. }
  201. /**
  202. * @brief returns the internal ownCloud name for the given LDAP DN of the group
  203. * @param $dn the dn of the group object
  204. * @param $ldapname optional, the display name of the object
  205. * @returns string with with the name to use in ownCloud, false on DN outside of search DN
  206. *
  207. * returns the internal ownCloud name for the given LDAP DN of the
  208. * group, false on DN outside of search DN or failure
  209. */
  210. public function dn2groupname($dn, $ldapname = null) {
  211. //To avoid bypassing the base DN settings under certain circumstances
  212. //with the group support, check whether the provided DN matches one of
  213. //the given Bases
  214. if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
  215. return false;
  216. }
  217. return $this->dn2ocname($dn, $ldapname, false);
  218. }
  219. /**
  220. * @brief returns the internal ownCloud name for the given LDAP DN of the user
  221. * @param $dn the dn of the user object
  222. * @param $ldapname optional, the display name of the object
  223. * @returns string with with the name to use in ownCloud
  224. *
  225. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  226. */
  227. public function dn2username($dn, $ldapname = null) {
  228. //To avoid bypassing the base DN settings under certain circumstances
  229. //with the group support, check whether the provided DN matches one of
  230. //the given Bases
  231. if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseUsers)) {
  232. return false;
  233. }
  234. return $this->dn2ocname($dn, $ldapname, true);
  235. }
  236. /**
  237. * @brief returns an internal ownCloud name for the given LDAP DN
  238. * @param $dn the dn of the user object
  239. * @param $ldapname optional, the display name of the object
  240. * @param $isUser optional, wether it is a user object (otherwise group assumed)
  241. * @returns string with with the name to use in ownCloud
  242. *
  243. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN
  244. */
  245. public function dn2ocname($dn, $ldapname = null, $isUser = true) {
  246. $table = $this->getMapTable($isUser);
  247. if($isUser) {
  248. $fncFindMappedName = 'findMappedUser';
  249. $nameAttribute = $this->connection->ldapUserDisplayName;
  250. } else {
  251. $fncFindMappedName = 'findMappedGroup';
  252. $nameAttribute = $this->connection->ldapGroupDisplayName;
  253. }
  254. //let's try to retrieve the ownCloud name from the mappings table
  255. $ocname = $this->$fncFindMappedName($dn);
  256. if($ocname) {
  257. return $ocname;
  258. }
  259. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  260. $uuid = $this->getUUID($dn);
  261. if($uuid) {
  262. $query = \OCP\DB::prepare('
  263. SELECT `owncloud_name`
  264. FROM `'.$table.'`
  265. WHERE `directory_uuid` = ?
  266. ');
  267. $component = $query->execute(array($uuid))->fetchOne();
  268. if($component) {
  269. $query = \OCP\DB::prepare('
  270. UPDATE `'.$table.'`
  271. SET `ldap_dn` = ?
  272. WHERE `directory_uuid` = ?
  273. ');
  274. $query->execute(array($dn, $uuid));
  275. return $component;
  276. }
  277. } else {
  278. //If the UUID can't be detected something is foul.
  279. \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$dn.'. Skipping.', \OCP\Util::INFO);
  280. return false;
  281. }
  282. if(is_null($ldapname)) {
  283. $ldapname = $this->readAttribute($dn, $nameAttribute);
  284. if(!isset($ldapname[0]) && empty($ldapname[0])) {
  285. \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$dn.'.', \OCP\Util::INFO);
  286. return false;
  287. }
  288. $ldapname = $ldapname[0];
  289. }
  290. if($isUser) {
  291. $usernameAttribute = $this->connection->ldapExpertUsernameAttr;
  292. if(!emptY($usernameAttribute)) {
  293. $username = $this->readAttribute($dn, $usernameAttribute);
  294. $username = $username[0];
  295. } else {
  296. $username = $uuid;
  297. }
  298. $intname = $this->sanitizeUsername($username);
  299. } else {
  300. $intname = $ldapname;
  301. }
  302. //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
  303. //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
  304. $originalTTL = $this->connection->ldapCacheTTL;
  305. $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
  306. if(($isUser && !\OCP\User::userExists($intname))
  307. || (!$isUser && !\OC_Group::groupExists($intname))) {
  308. if($this->mapComponent($dn, $intname, $isUser)) {
  309. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  310. return $intname;
  311. }
  312. }
  313. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  314. $altname = $this->createAltInternalOwnCloudName($intname, $isUser);
  315. if($this->mapComponent($dn, $altname, $isUser)) {
  316. return $altname;
  317. }
  318. //if everything else did not help..
  319. \OCP\Util::writeLog('user_ldap', 'Could not create unique ownCloud name for '.$dn.'.', \OCP\Util::INFO);
  320. return false;
  321. }
  322. /**
  323. * @brief gives back the user names as they are used ownClod internally
  324. * @param $ldapGroups an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
  325. * @returns an array with the user names to use in ownCloud
  326. *
  327. * gives back the user names as they are used ownClod internally
  328. */
  329. public function ownCloudUserNames($ldapUsers) {
  330. return $this->ldap2ownCloudNames($ldapUsers, true);
  331. }
  332. /**
  333. * @brief gives back the group names as they are used ownClod internally
  334. * @param $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
  335. * @returns an array with the group names to use in ownCloud
  336. *
  337. * gives back the group names as they are used ownClod internally
  338. */
  339. public function ownCloudGroupNames($ldapGroups) {
  340. return $this->ldap2ownCloudNames($ldapGroups, false);
  341. }
  342. private function findMappedUser($dn) {
  343. static $query = null;
  344. if(is_null($query)) {
  345. $query = \OCP\DB::prepare('
  346. SELECT `owncloud_name`
  347. FROM `'.$this->getMapTable(true).'`
  348. WHERE `ldap_dn` = ?'
  349. );
  350. }
  351. $res = $query->execute(array($dn))->fetchOne();
  352. if($res) {
  353. return $res;
  354. }
  355. return false;
  356. }
  357. private function findMappedGroup($dn) {
  358. static $query = null;
  359. if(is_null($query)) {
  360. $query = \OCP\DB::prepare('
  361. SELECT `owncloud_name`
  362. FROM `'.$this->getMapTable(false).'`
  363. WHERE `ldap_dn` = ?'
  364. );
  365. }
  366. $res = $query->execute(array($dn))->fetchOne();
  367. if($res) {
  368. return $res;
  369. }
  370. return false;
  371. }
  372. private function ldap2ownCloudNames($ldapObjects, $isUsers) {
  373. if($isUsers) {
  374. $nameAttribute = $this->connection->ldapUserDisplayName;
  375. } else {
  376. $nameAttribute = $this->connection->ldapGroupDisplayName;
  377. }
  378. $ownCloudNames = array();
  379. foreach($ldapObjects as $ldapObject) {
  380. $nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null;
  381. $ocname = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers);
  382. if($ocname) {
  383. $ownCloudNames[] = $ocname;
  384. }
  385. continue;
  386. }
  387. return $ownCloudNames;
  388. }
  389. /**
  390. * @brief creates a unique name for internal ownCloud use for users. Don't call it directly.
  391. * @param $name the display name of the object
  392. * @returns string with with the name to use in ownCloud or false if unsuccessful
  393. *
  394. * Instead of using this method directly, call
  395. * createAltInternalOwnCloudName($name, true)
  396. */
  397. private function _createAltInternalOwnCloudNameForUsers($name) {
  398. $attempts = 0;
  399. //while loop is just a precaution. If a name is not generated within
  400. //20 attempts, something else is very wrong. Avoids infinite loop.
  401. while($attempts < 20){
  402. $altName = $name . '_' . rand(1000,9999);
  403. if(!\OCP\User::userExists($altName)) {
  404. return $altName;
  405. }
  406. $attempts++;
  407. }
  408. return false;
  409. }
  410. /**
  411. * @brief creates a unique name for internal ownCloud use for groups. Don't call it directly.
  412. * @param $name the display name of the object
  413. * @returns string with with the name to use in ownCloud or false if unsuccessful.
  414. *
  415. * Instead of using this method directly, call
  416. * createAltInternalOwnCloudName($name, false)
  417. *
  418. * Group names are also used as display names, so we do a sequential
  419. * numbering, e.g. Developers_42 when there are 41 other groups called
  420. * "Developers"
  421. */
  422. private function _createAltInternalOwnCloudNameForGroups($name) {
  423. $query = \OCP\DB::prepare('
  424. SELECT `owncloud_name`
  425. FROM `'.$this->getMapTable(false).'`
  426. WHERE `owncloud_name` LIKE ?
  427. ');
  428. $usedNames = array();
  429. $res = $query->execute(array($name.'_%'));
  430. while($row = $res->fetchRow()) {
  431. $usedNames[] = $row['owncloud_name'];
  432. }
  433. if(!($usedNames) || count($usedNames) === 0) {
  434. $lastNo = 1; //will become name_2
  435. } else {
  436. natsort($usedNames);
  437. $lastname = array_pop($usedNames);
  438. $lastNo = intval(substr($lastname, strrpos($lastname, '_') + 1));
  439. }
  440. $altName = $name.'_'.strval($lastNo+1);
  441. unset($usedNames);
  442. $attempts = 1;
  443. while($attempts < 21){
  444. //Pro forma check to be really sure it is unique
  445. //while loop is just a precaution. If a name is not generated within
  446. //20 attempts, something else is very wrong. Avoids infinite loop.
  447. if(!\OC_Group::groupExists($altName)) {
  448. return $altName;
  449. }
  450. $altName = $name . '_' . $lastNo + $attempts;
  451. $attempts++;
  452. }
  453. return false;
  454. }
  455. /**
  456. * @brief creates a unique name for internal ownCloud use.
  457. * @param $name the display name of the object
  458. * @param $isUser boolean, whether name should be created for a user (true) or a group (false)
  459. * @returns string with with the name to use in ownCloud or false if unsuccessful
  460. */
  461. private function createAltInternalOwnCloudName($name, $isUser) {
  462. $originalTTL = $this->connection->ldapCacheTTL;
  463. $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
  464. if($isUser) {
  465. $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
  466. } else {
  467. $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
  468. }
  469. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  470. return $altName;
  471. }
  472. /**
  473. * @brief retrieves all known groups from the mappings table
  474. * @returns array with the results
  475. *
  476. * retrieves all known groups from the mappings table
  477. */
  478. private function mappedGroups() {
  479. return $this->mappedComponents(false);
  480. }
  481. /**
  482. * @brief retrieves all known users from the mappings table
  483. * @returns array with the results
  484. *
  485. * retrieves all known users from the mappings table
  486. */
  487. private function mappedUsers() {
  488. return $this->mappedComponents(true);
  489. }
  490. private function mappedComponents($isUsers) {
  491. $table = $this->getMapTable($isUsers);
  492. $query = \OCP\DB::prepare('
  493. SELECT `ldap_dn`, `owncloud_name`
  494. FROM `'. $table . '`'
  495. );
  496. return $query->execute()->fetchAll();
  497. }
  498. /**
  499. * @brief inserts a new user or group into the mappings table
  500. * @param $dn the record in question
  501. * @param $ocname the name to use in ownCloud
  502. * @param $isUser is it a user or a group?
  503. * @returns true on success, false otherwise
  504. *
  505. * inserts a new user or group into the mappings table
  506. */
  507. private function mapComponent($dn, $ocname, $isUser = true) {
  508. $table = $this->getMapTable($isUser);
  509. $sqlAdjustment = '';
  510. $dbtype = \OCP\Config::getSystemValue('dbtype');
  511. if($dbtype === 'mysql') {
  512. $sqlAdjustment = 'FROM DUAL';
  513. }
  514. $insert = \OCP\DB::prepare('
  515. INSERT INTO `'.$table.'` (`ldap_dn`, `owncloud_name`, `directory_uuid`)
  516. SELECT ?,?,?
  517. '.$sqlAdjustment.'
  518. WHERE NOT EXISTS (
  519. SELECT 1
  520. FROM `'.$table.'`
  521. WHERE `ldap_dn` = ?
  522. OR `owncloud_name` = ?)
  523. ');
  524. //feed the DB
  525. $insRows = $insert->execute(array($dn, $ocname, $this->getUUID($dn), $dn, $ocname));
  526. if(\OCP\DB::isError($insRows)) {
  527. return false;
  528. }
  529. if($insRows === 0) {
  530. return false;
  531. }
  532. return true;
  533. }
  534. public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
  535. return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1));
  536. }
  537. public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
  538. return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
  539. }
  540. private function fetchList($list, $manyAttributes) {
  541. if(is_array($list)) {
  542. if($manyAttributes) {
  543. return $list;
  544. } else {
  545. return array_unique($list, SORT_LOCALE_STRING);
  546. }
  547. }
  548. //error cause actually, maybe throw an exception in future.
  549. return array();
  550. }
  551. /**
  552. * @brief executes an LDAP search, optimized for Users
  553. * @param $filter the LDAP filter for the search
  554. * @param $attr optional, when a certain attribute shall be filtered out
  555. * @returns array with the search result
  556. *
  557. * Executes an LDAP search
  558. */
  559. public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
  560. return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
  561. }
  562. /**
  563. * @brief executes an LDAP search, optimized for Groups
  564. * @param $filter the LDAP filter for the search
  565. * @param $attr optional, when a certain attribute shall be filtered out
  566. * @returns array with the search result
  567. *
  568. * Executes an LDAP search
  569. */
  570. public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
  571. return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
  572. }
  573. /**
  574. * @brief executes an LDAP search
  575. * @param $filter the LDAP filter for the search
  576. * @param $base an array containing the LDAP subtree(s) that shall be searched
  577. * @param $attr optional, array, one or more attributes that shall be
  578. * retrieved. Results will according to the order in the array.
  579. * @returns array with the search result
  580. *
  581. * Executes an LDAP search
  582. */
  583. private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
  584. if(!is_null($attr) && !is_array($attr)) {
  585. $attr = array(mb_strtolower($attr, 'UTF-8'));
  586. }
  587. // See if we have a resource, in case not cancel with message
  588. $link_resource = $this->connection->getConnectionResource();
  589. if(!is_resource($link_resource)) {
  590. // Seems like we didn't find any resource.
  591. // Return an empty array just like before.
  592. \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
  593. return array();
  594. }
  595. //check wether paged search should be attempted
  596. $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, $limit, $offset);
  597. $linkResources = array_pad(array(), count($base), $link_resource);
  598. $sr = ldap_search($linkResources, $base, $filter, $attr);
  599. $error = ldap_errno($link_resource);
  600. if(!is_array($sr) || $error !== 0) {
  601. \OCP\Util::writeLog('user_ldap',
  602. 'Error when searching: '.ldap_error($link_resource).' code '.ldap_errno($link_resource),
  603. \OCP\Util::ERROR);
  604. \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
  605. return array();
  606. }
  607. // Do the server-side sorting
  608. foreach(array_reverse($attr) as $sortAttr){
  609. foreach($sr as $searchResource) {
  610. ldap_sort($link_resource, $searchResource, $sortAttr);
  611. }
  612. }
  613. $findings = array();
  614. foreach($sr as $key => $res) {
  615. $findings = array_merge($findings, ldap_get_entries($link_resource, $res ));
  616. }
  617. if($pagedSearchOK) {
  618. \OCP\Util::writeLog('user_ldap', 'Paged search successful', \OCP\Util::INFO);
  619. foreach($sr as $key => $res) {
  620. $cookie = null;
  621. if(ldap_control_paged_result_response($link_resource, $res, $cookie)) {
  622. \OCP\Util::writeLog('user_ldap', 'Set paged search cookie', \OCP\Util::INFO);
  623. $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
  624. }
  625. }
  626. //browsing through prior pages to get the cookie for the new one
  627. if($skipHandling) {
  628. return;
  629. }
  630. // if count is bigger, then the server does not support
  631. // paged search. Instead, he did a normal search. We set a
  632. // flag here, so the callee knows how to deal with it.
  633. if($findings['count'] <= $limit) {
  634. $this->pagedSearchedSuccessful = true;
  635. }
  636. } else {
  637. if(!is_null($limit)) {
  638. \OCP\Util::writeLog('user_ldap', 'Paged search failed :(', \OCP\Util::INFO);
  639. }
  640. }
  641. // if we're here, probably no connection resource is returned.
  642. // to make ownCloud behave nicely, we simply give back an empty array.
  643. if(is_null($findings)) {
  644. return array();
  645. }
  646. if(!is_null($attr)) {
  647. $selection = array();
  648. $multiarray = false;
  649. if(count($attr) > 1) {
  650. $multiarray = true;
  651. $i = 0;
  652. }
  653. foreach($findings as $item) {
  654. if(!is_array($item)) {
  655. continue;
  656. }
  657. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  658. if($multiarray) {
  659. foreach($attr as $key) {
  660. $key = mb_strtolower($key, 'UTF-8');
  661. if(isset($item[$key])) {
  662. if($key !== 'dn') {
  663. $selection[$i][$key] = $this->resemblesDN($key) ?
  664. $this->sanitizeDN($item[$key][0])
  665. : $item[$key][0];
  666. } else {
  667. $selection[$i][$key] = $this->sanitizeDN($item[$key]);
  668. }
  669. }
  670. }
  671. $i++;
  672. } else {
  673. //tribute to case insensitivity
  674. $key = mb_strtolower($attr[0], 'UTF-8');
  675. if(isset($item[$key])) {
  676. if($this->resemblesDN($key)) {
  677. $selection[] = $this->sanitizeDN($item[$key]);
  678. } else {
  679. $selection[] = $item[$key];
  680. }
  681. }
  682. }
  683. }
  684. $findings = $selection;
  685. }
  686. //we slice the findings, when
  687. //a) paged search insuccessful, though attempted
  688. //b) no paged search, but limit set
  689. if((!$this->pagedSearchedSuccessful
  690. && $pagedSearchOK)
  691. || (
  692. !$pagedSearchOK
  693. && !is_null($limit)
  694. )
  695. ) {
  696. $findings = array_slice($findings, intval($offset), $limit);
  697. }
  698. return $findings;
  699. }
  700. public function sanitizeUsername($name) {
  701. if($this->connection->ldapIgnoreNamingRules) {
  702. return $name;
  703. }
  704. // Translitaration
  705. //latin characters to ASCII
  706. $name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
  707. //REPLACEMENTS
  708. $name = \OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
  709. //every remaining unallowed characters will be removed
  710. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  711. return $name;
  712. }
  713. /**
  714. * @brief combines the input filters with AND
  715. * @param $filters array, the filters to connect
  716. * @returns the combined filter
  717. *
  718. * Combines Filter arguments with AND
  719. */
  720. public function combineFilterWithAnd($filters) {
  721. return $this->combineFilter($filters, '&');
  722. }
  723. /**
  724. * @brief combines the input filters with AND
  725. * @param $filters array, the filters to connect
  726. * @returns the combined filter
  727. *
  728. * Combines Filter arguments with AND
  729. */
  730. public function combineFilterWithOr($filters) {
  731. return $this->combineFilter($filters, '|');
  732. }
  733. /**
  734. * @brief combines the input filters with given operator
  735. * @param $filters array, the filters to connect
  736. * @param $operator either & or |
  737. * @returns the combined filter
  738. *
  739. * Combines Filter arguments with AND
  740. */
  741. private function combineFilter($filters, $operator) {
  742. $combinedFilter = '('.$operator;
  743. foreach($filters as $filter) {
  744. if($filter[0] !== '(') {
  745. $filter = '('.$filter.')';
  746. }
  747. $combinedFilter.=$filter;
  748. }
  749. $combinedFilter.=')';
  750. return $combinedFilter;
  751. }
  752. /**
  753. * @brief creates a filter part for to perfrom search for users
  754. * @param string $search the search term
  755. * @return string the final filter part to use in LDAP searches
  756. */
  757. public function getFilterPartForUserSearch($search) {
  758. return $this->getFilterPartForSearch($search,
  759. $this->connection->ldapAttributesForUserSearch,
  760. $this->connection->ldapUserDisplayName);
  761. }
  762. /**
  763. * @brief creates a filter part for to perfrom search for groups
  764. * @param string $search the search term
  765. * @return string the final filter part to use in LDAP searches
  766. */
  767. public function getFilterPartForGroupSearch($search) {
  768. return $this->getFilterPartForSearch($search,
  769. $this->connection->ldapAttributesForGroupSearch,
  770. $this->connection->ldapGroupDisplayName);
  771. }
  772. /**
  773. * @brief creates a filter part for searches
  774. * @param string $search the search term
  775. * @param string $fallbackAttribute a fallback attribute in case the user
  776. * did not define search attributes. Typically the display name attribute.
  777. * @returns string the final filter part to use in LDAP searches
  778. */
  779. private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
  780. $filter = array();
  781. $search = empty($search) ? '*' : '*'.$search.'*';
  782. if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
  783. if(empty($fallbackAttribute)) {
  784. return '';
  785. }
  786. $filter[] = $fallbackAttribute . '=' . $search;
  787. } else {
  788. foreach($searchAttributes as $attribute) {
  789. $filter[] = $attribute . '=' . $search;
  790. }
  791. }
  792. if(count($filter) === 1) {
  793. return '('.$filter[0].')';
  794. }
  795. return $this->combineFilterWithOr($filter);
  796. }
  797. public function areCredentialsValid($name, $password) {
  798. $name = $this->DNasBaseParameter($name);
  799. $testConnection = clone $this->connection;
  800. $credentials = array(
  801. 'ldapAgentName' => $name,
  802. 'ldapAgentPassword' => $password
  803. );
  804. if(!$testConnection->setConfiguration($credentials)) {
  805. return false;
  806. }
  807. return $testConnection->bind();
  808. }
  809. /**
  810. * @brief auto-detects the directory's UUID attribute
  811. * @param $dn a known DN used to check against
  812. * @param $force the detection should be run, even if it is not set to auto
  813. * @returns true on success, false otherwise
  814. */
  815. private function detectUuidAttribute($dn, $force = false) {
  816. if(($this->connection->ldapUuidAttribute !== 'auto') && !$force) {
  817. return true;
  818. }
  819. $fixedAttribute = $this->connection->ldapExpertUUIDAttr;
  820. if(!empty($fixedAttribute)) {
  821. $this->connection->ldapUuidAttribute = $fixedAttribute;
  822. return true;
  823. }
  824. //for now, supported (known) attributes are entryUUID, nsuniqueid, objectGUID
  825. $testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid');
  826. foreach($testAttributes as $attribute) {
  827. \OCP\Util::writeLog('user_ldap', 'Testing '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  828. $value = $this->readAttribute($dn, $attribute);
  829. if(is_array($value) && isset($value[0]) && !empty($value[0])) {
  830. \OCP\Util::writeLog('user_ldap', 'Setting '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  831. $this->connection->ldapUuidAttribute = $attribute;
  832. return true;
  833. }
  834. \OCP\Util::writeLog('user_ldap',
  835. 'The looked for uuid attr is not '.$attribute.', result was '.print_r($value, true),
  836. \OCP\Util::DEBUG);
  837. }
  838. return false;
  839. }
  840. public function getUUID($dn) {
  841. if($this->detectUuidAttribute($dn)) {
  842. \OCP\Util::writeLog('user_ldap',
  843. 'UUID Checking \ UUID for '.$dn.' using '. $this->connection->ldapUuidAttribute,
  844. \OCP\Util::DEBUG);
  845. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  846. if(!is_array($uuid) && $this->connection->ldapOverrideUuidAttribute) {
  847. $this->detectUuidAttribute($dn, true);
  848. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  849. }
  850. if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
  851. $uuid = $uuid[0];
  852. } else {
  853. $uuid = false;
  854. }
  855. } else {
  856. $uuid = false;
  857. }
  858. return $uuid;
  859. }
  860. /**
  861. * @brief converts a binary ObjectGUID into a string representation
  862. * @param $oguid the ObjectGUID in it's binary form as retrieved from AD
  863. * @returns String
  864. *
  865. * converts a binary ObjectGUID into a string representation
  866. * http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  867. */
  868. private function convertObjectGUID2Str($oguid) {
  869. $hex_guid = bin2hex($oguid);
  870. $hex_guid_to_guid_str = '';
  871. for($k = 1; $k <= 4; ++$k) {
  872. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  873. }
  874. $hex_guid_to_guid_str .= '-';
  875. for($k = 1; $k <= 2; ++$k) {
  876. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  877. }
  878. $hex_guid_to_guid_str .= '-';
  879. for($k = 1; $k <= 2; ++$k) {
  880. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  881. }
  882. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  883. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  884. return strtoupper($hex_guid_to_guid_str);
  885. }
  886. /**
  887. * @brief converts a stored DN so it can be used as base parameter for LDAP queries
  888. * @param $dn the DN
  889. * @returns String
  890. *
  891. * converts a stored DN so it can be used as base parameter for LDAP queries
  892. * internally we store them for usage in LDAP filters
  893. */
  894. private function DNasBaseParameter($dn) {
  895. return str_replace('\\5c', '\\', $dn);
  896. }
  897. /**
  898. * @brief checks if the given DN is part of the given base DN(s)
  899. * @param $dn the DN
  900. * @param $bases array containing the allowed base DN or DNs
  901. * @returns Boolean
  902. */
  903. private function isDNPartOfBase($dn, $bases) {
  904. $bases = $this->sanitizeDN($bases);
  905. foreach($bases as $base) {
  906. $belongsToBase = true;
  907. if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base))) {
  908. $belongsToBase = false;
  909. }
  910. if($belongsToBase) {
  911. break;
  912. }
  913. }
  914. return $belongsToBase;
  915. }
  916. /**
  917. * @brief get a cookie for the next LDAP paged search
  918. * @param $base a string with the base DN for the search
  919. * @param $filter the search filter to identify the correct search
  920. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  921. * @param $offset the offset for the new search to identify the correct search really good
  922. * @returns string containing the key or empty if none is cached
  923. */
  924. private function getPagedResultCookie($base, $filter, $limit, $offset) {
  925. if($offset === 0) {
  926. return '';
  927. }
  928. $offset -= $limit;
  929. //we work with cache here
  930. $cachekey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . $limit . '-' . $offset;
  931. $cookie = $this->connection->getFromCache($cachekey);
  932. if(is_null($cookie)) {
  933. $cookie = '';
  934. }
  935. return $cookie;
  936. }
  937. /**
  938. * @brief set a cookie for LDAP paged search run
  939. * @param $base a string with the base DN for the search
  940. * @param $filter the search filter to identify the correct search
  941. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  942. * @param $offset the offset for the run search to identify the correct search really good
  943. * @param $cookie string containing the cookie returned by ldap_control_paged_result_response
  944. * @return void
  945. */
  946. private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
  947. if(!empty($cookie)) {
  948. $cachekey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .$limit . '-' . $offset;
  949. $cookie = $this->connection->writeToCache($cachekey, $cookie);
  950. }
  951. }
  952. /**
  953. * @brief check wether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  954. * @return true on success, null or false otherwise
  955. */
  956. public function getPagedSearchResultState() {
  957. $result = $this->pagedSearchedSuccessful;
  958. $this->pagedSearchedSuccessful = null;
  959. return $result;
  960. }
  961. /**
  962. * @brief prepares a paged search, if possible
  963. * @param $filter the LDAP filter for the search
  964. * @param $bases an array containing the LDAP subtree(s) that shall be searched
  965. * @param $attr optional, when a certain attribute shall be filtered outside
  966. * @param $limit
  967. * @param $offset
  968. *
  969. */
  970. private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
  971. $pagedSearchOK = false;
  972. if($this->connection->hasPagedResultSupport && !is_null($limit)) {
  973. $offset = intval($offset); //can be null
  974. \OCP\Util::writeLog('user_ldap',
  975. 'initializing paged search for Filter'.$filter.' base '.print_r($bases, true)
  976. .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
  977. \OCP\Util::INFO);
  978. //get the cookie from the search for the previous search, required by LDAP
  979. foreach($bases as $base) {
  980. $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
  981. if(empty($cookie) && ($offset > 0)) {
  982. // no cookie known, although the offset is not 0. Maybe cache run out. We need
  983. // to start all over *sigh* (btw, Dear Reader, did you need LDAP paged
  984. // searching was designed by MSFT?)
  985. $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
  986. //a bit recursive, $offset of 0 is the exit
  987. \OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
  988. $this->search($filter, array($base), $attr, $limit, $reOffset, true);
  989. $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
  990. //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
  991. //TODO: remember this, probably does not change in the next request...
  992. if(empty($cookie)) {
  993. $cookie = null;
  994. }
  995. }
  996. if(!is_null($cookie)) {
  997. if($offset > 0) {
  998. \OCP\Util::writeLog('user_ldap', 'Cookie '.$cookie, \OCP\Util::INFO);
  999. }
  1000. $pagedSearchOK = ldap_control_paged_result($this->connection->getConnectionResource(),
  1001. $limit, false, $cookie);
  1002. if(!$pagedSearchOK) {
  1003. return false;
  1004. }
  1005. \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::INFO);
  1006. } else {
  1007. \OCP\Util::writeLog('user_ldap',
  1008. 'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
  1009. \OCP\Util::INFO);
  1010. }
  1011. }
  1012. }
  1013. return $pagedSearchOK;
  1014. }
  1015. }