access.php 36 KB

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