You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2083 lines
64 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
13 years ago
13 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
LDAP Wizard Overhaul wizard refactor reimplement save spinners and cursor implement Port detector introduced detector queue, added base dn detector disable input fields when detectors are running introduce spinners for fields that are being updated by detector cache jq element objects consolidate processing of detector results in generic / abstract base class display notification if a detector discovered a problem don't run base dn detector if a base is configured reset detector queue on configuration switch implement functionality check and update of status indicator document ConfigModel jsdoc for controller and main view more documentation implement the user filter tab view so far the multiselects get initialized (not filled yet) and the mode can be switched. mode is also restored. reintroduce filter switch confirmation in admin XP mode new detector for user object classes. so we also load user object classes if necessary and are able to save and show the setting. multiselect trigger save actions now on close only show spinners automatically, when a detector is running 20k limit for object classes preselection test adjust wordings, fix grammar add group (for users tab) detector also includes wording fixes error presentation moved from detectors to view, where it belongs add info label to users page missing wording changes show effective LDAP filter in Assisted Mode add user filter detector implement count button for users and limit all count actions to 1001 for performance reasons make port field a bit bigger. not perfect though. do not detect port automatically implement login filter tab view only load features in assisted mode and don't enable assisted fields while in raw mode add tooltips on login filter checkbox options for better understanding permanently show filter on login tab and also compile login filter in assisted mode test/verify button on login attributes tab, with backend changes. only run wizard requests if your an active tab. also run compile filter requests when switching to assisted mode underline toggle filter links to stress that they are clickable unity user and group tab functionality in common abstract class, add group filter tab view. only detectors and template adjustments left to have group tab implementation complete add object class and group detector for groups as well as filter composer show ldap filter permanently on groups tab introduce input element that can deal better with many groups, will be used with > 40 fix disabling complex group chooser while detection is running hide complex group chooser on config switch fix few more issues with complex chooser make complex group chooser available on Users tab as well detect base dn improvements/changes: - do not look for Base DN automatically, offer a button instead - fix for alternative way to detect a base dn (if agent dn is not given) - do not trigger filter composers on config switch Changes with configuration chooser controls - "New" was removed out of the configuration list - and split into buttons "add" and "copy" - delete button is also now an icon add test button for Base DN reimplement advanced tab. The save button is gone. reimplement expert tab remove unused methods implement mail attribute detector implement user display name attribute detection implement member group association detector replace text input with textarea for raw filter input finish functionality check auto-enable good configurations, as it was before cleanup move save confirmation handling to base class, reduces code duplication enable tabs only if no running save processes are left. move onConfigLoaded to base class, avoids code duplication simplify, save LOCs Test Configuration button to be dealt with in main view as it is a cross-tab element require detectorQueue in constructor cleanup put bootstrap into a function and thus make it testable get rid of old stuff
11 years ago
14 years ago
14 years ago
13 years ago
13 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Aaron Wood <aaronjwood@gmail.com>
  6. * @author Andreas Fischer <bantu@owncloud.com>
  7. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  8. * @author Bart Visscher <bartv@thisnet.nl>
  9. * @author Benjamin Diele <benjamin@diele.be>
  10. * @author bline <scottbeck@gmail.com>
  11. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  12. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  13. * @author Joas Schilling <coding@schilljs.com>
  14. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  15. * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
  16. * @author Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
  17. * @author Lukas Reschke <lukas@statuscode.ch>
  18. * @author Mario Kolling <mario.kolling@serpro.gov.br>
  19. * @author Max Kovalenko <mxss1998@yandex.ru>
  20. * @author Morris Jobke <hey@morrisjobke.de>
  21. * @author Nicolas Grekas <nicolas.grekas@gmail.com>
  22. * @author Peter Kubica <peter@kubica.ch>
  23. * @author Ralph Krimmel <rkrimme1@gwdg.de>
  24. * @author Robin McCorkell <robin@mccorkell.me.uk>
  25. * @author Roeland Jago Douma <roeland@famdouma.nl>
  26. * @author Roger Szabo <roger.szabo@web.de>
  27. * @author Roland Tapken <roland@bitarbeiter.net>
  28. * @author root <root@localhost.localdomain>
  29. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  30. *
  31. * @license AGPL-3.0
  32. *
  33. * This code is free software: you can redistribute it and/or modify
  34. * it under the terms of the GNU Affero General Public License, version 3,
  35. * as published by the Free Software Foundation.
  36. *
  37. * This program is distributed in the hope that it will be useful,
  38. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  39. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  40. * GNU Affero General Public License for more details.
  41. *
  42. * You should have received a copy of the GNU Affero General Public License, version 3,
  43. * along with this program. If not, see <http://www.gnu.org/licenses/>
  44. *
  45. */
  46. namespace OCA\User_LDAP;
  47. use OC\HintException;
  48. use OC\Hooks\PublicEmitter;
  49. use OC\ServerNotAvailableException;
  50. use OCA\User_LDAP\Exceptions\ConstraintViolationException;
  51. use OCA\User_LDAP\Mapping\AbstractMapping;
  52. use OCA\User_LDAP\User\Manager;
  53. use OCA\User_LDAP\User\OfflineUser;
  54. use OCP\IConfig;
  55. use OCP\ILogger;
  56. use OCP\IUserManager;
  57. /**
  58. * Class Access
  59. *
  60. * @package OCA\User_LDAP
  61. */
  62. class Access extends LDAPUtility {
  63. public const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
  64. /** @var \OCA\User_LDAP\Connection */
  65. public $connection;
  66. /** @var Manager */
  67. public $userManager;
  68. //never ever check this var directly, always use getPagedSearchResultState
  69. protected $pagedSearchedSuccessful;
  70. /**
  71. * protected $cookies = [];
  72. *
  73. * @var AbstractMapping $userMapper
  74. */
  75. protected $userMapper;
  76. /**
  77. * @var AbstractMapping $userMapper
  78. */
  79. protected $groupMapper;
  80. /**
  81. * @var \OCA\User_LDAP\Helper
  82. */
  83. private $helper;
  84. /** @var IConfig */
  85. private $config;
  86. /** @var IUserManager */
  87. private $ncUserManager;
  88. /** @var string */
  89. private $lastCookie = '';
  90. public function __construct(
  91. Connection $connection,
  92. ILDAPWrapper $ldap,
  93. Manager $userManager,
  94. Helper $helper,
  95. IConfig $config,
  96. IUserManager $ncUserManager
  97. ) {
  98. parent::__construct($ldap);
  99. $this->connection = $connection;
  100. $this->userManager = $userManager;
  101. $this->userManager->setLdapAccess($this);
  102. $this->helper = $helper;
  103. $this->config = $config;
  104. $this->ncUserManager = $ncUserManager;
  105. }
  106. /**
  107. * sets the User Mapper
  108. *
  109. * @param AbstractMapping $mapper
  110. */
  111. public function setUserMapper(AbstractMapping $mapper) {
  112. $this->userMapper = $mapper;
  113. }
  114. /**
  115. * returns the User Mapper
  116. *
  117. * @return AbstractMapping
  118. * @throws \Exception
  119. */
  120. public function getUserMapper() {
  121. if (is_null($this->userMapper)) {
  122. throw new \Exception('UserMapper was not assigned to this Access instance.');
  123. }
  124. return $this->userMapper;
  125. }
  126. /**
  127. * sets the Group Mapper
  128. *
  129. * @param AbstractMapping $mapper
  130. */
  131. public function setGroupMapper(AbstractMapping $mapper) {
  132. $this->groupMapper = $mapper;
  133. }
  134. /**
  135. * returns the Group Mapper
  136. *
  137. * @return AbstractMapping
  138. * @throws \Exception
  139. */
  140. public function getGroupMapper() {
  141. if (is_null($this->groupMapper)) {
  142. throw new \Exception('GroupMapper was not assigned to this Access instance.');
  143. }
  144. return $this->groupMapper;
  145. }
  146. /**
  147. * @return bool
  148. */
  149. private function checkConnection() {
  150. return ($this->connection instanceof Connection);
  151. }
  152. /**
  153. * returns the Connection instance
  154. *
  155. * @return \OCA\User_LDAP\Connection
  156. */
  157. public function getConnection() {
  158. return $this->connection;
  159. }
  160. /**
  161. * reads a given attribute for an LDAP record identified by a DN
  162. *
  163. * @param string $dn the record in question
  164. * @param string $attr the attribute that shall be retrieved
  165. * if empty, just check the record's existence
  166. * @param string $filter
  167. * @return array|false an array of values on success or an empty
  168. * array if $attr is empty, false otherwise
  169. * @throws ServerNotAvailableException
  170. */
  171. public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
  172. if (!$this->checkConnection()) {
  173. \OCP\Util::writeLog('user_ldap',
  174. 'No LDAP Connector assigned, access impossible for readAttribute.',
  175. ILogger::WARN);
  176. return false;
  177. }
  178. $cr = $this->connection->getConnectionResource();
  179. if (!$this->ldap->isResource($cr)) {
  180. //LDAP not available
  181. \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
  182. return false;
  183. }
  184. //Cancel possibly running Paged Results operation, otherwise we run in
  185. //LDAP protocol errors
  186. $this->abandonPagedSearch();
  187. // openLDAP requires that we init a new Paged Search. Not needed by AD,
  188. // but does not hurt either.
  189. $pagingSize = (int)$this->connection->ldapPagingSize;
  190. // 0 won't result in replies, small numbers may leave out groups
  191. // (cf. #12306), 500 is default for paging and should work everywhere.
  192. $maxResults = $pagingSize > 20 ? $pagingSize : 500;
  193. $attr = mb_strtolower($attr, 'UTF-8');
  194. // the actual read attribute later may contain parameters on a ranged
  195. // request, e.g. member;range=99-199. Depends on server reply.
  196. $attrToRead = $attr;
  197. $values = [];
  198. $isRangeRequest = false;
  199. do {
  200. $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
  201. if (is_bool($result)) {
  202. // when an exists request was run and it was successful, an empty
  203. // array must be returned
  204. return $result ? [] : false;
  205. }
  206. if (!$isRangeRequest) {
  207. $values = $this->extractAttributeValuesFromResult($result, $attr);
  208. if (!empty($values)) {
  209. return $values;
  210. }
  211. }
  212. $isRangeRequest = false;
  213. $result = $this->extractRangeData($result, $attr);
  214. if (!empty($result)) {
  215. $normalizedResult = $this->extractAttributeValuesFromResult(
  216. [$attr => $result['values']],
  217. $attr
  218. );
  219. $values = array_merge($values, $normalizedResult);
  220. if ($result['rangeHigh'] === '*') {
  221. // when server replies with * as high range value, there are
  222. // no more results left
  223. return $values;
  224. } else {
  225. $low = $result['rangeHigh'] + 1;
  226. $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
  227. $isRangeRequest = true;
  228. }
  229. }
  230. } while ($isRangeRequest);
  231. \OCP\Util::writeLog('user_ldap', 'Requested attribute ' . $attr . ' not found for ' . $dn, ILogger::DEBUG);
  232. return false;
  233. }
  234. /**
  235. * Runs an read operation against LDAP
  236. *
  237. * @param resource $cr the LDAP connection
  238. * @param string $dn
  239. * @param string $attribute
  240. * @param string $filter
  241. * @param int $maxResults
  242. * @return array|bool false if there was any error, true if an exists check
  243. * was performed and the requested DN found, array with the
  244. * returned data on a successful usual operation
  245. * @throws ServerNotAvailableException
  246. */
  247. public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
  248. $this->initPagedSearch($filter, $dn, [$attribute], $maxResults, 0);
  249. $dn = $this->helper->DNasBaseParameter($dn);
  250. $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, [$attribute]);
  251. if (!$this->ldap->isResource($rr)) {
  252. if ($attribute !== '') {
  253. //do not throw this message on userExists check, irritates
  254. \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
  255. }
  256. //in case an error occurs , e.g. object does not exist
  257. return false;
  258. }
  259. if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
  260. \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
  261. return true;
  262. }
  263. $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
  264. if (!$this->ldap->isResource($er)) {
  265. //did not match the filter, return false
  266. return false;
  267. }
  268. //LDAP attributes are not case sensitive
  269. $result = \OCP\Util::mb_array_change_key_case(
  270. $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
  271. return $result;
  272. }
  273. /**
  274. * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
  275. * data if present.
  276. *
  277. * @param array $result from ILDAPWrapper::getAttributes()
  278. * @param string $attribute the attribute name that was read
  279. * @return string[]
  280. */
  281. public function extractAttributeValuesFromResult($result, $attribute) {
  282. $values = [];
  283. if (isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
  284. $lowercaseAttribute = strtolower($attribute);
  285. for ($i = 0; $i < $result[$attribute]['count']; $i++) {
  286. if ($this->resemblesDN($attribute)) {
  287. $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
  288. } elseif ($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
  289. $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
  290. } else {
  291. $values[] = $result[$attribute][$i];
  292. }
  293. }
  294. }
  295. return $values;
  296. }
  297. /**
  298. * Attempts to find ranged data in a getAttribute results and extracts the
  299. * returned values as well as information on the range and full attribute
  300. * name for further processing.
  301. *
  302. * @param array $result from ILDAPWrapper::getAttributes()
  303. * @param string $attribute the attribute name that was read. Without ";range=…"
  304. * @return array If a range was detected with keys 'values', 'attributeName',
  305. * 'attributeFull' and 'rangeHigh', otherwise empty.
  306. */
  307. public function extractRangeData($result, $attribute) {
  308. $keys = array_keys($result);
  309. foreach ($keys as $key) {
  310. if ($key !== $attribute && strpos($key, $attribute) === 0) {
  311. $queryData = explode(';', $key);
  312. if (strpos($queryData[1], 'range=') === 0) {
  313. $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
  314. $data = [
  315. 'values' => $result[$key],
  316. 'attributeName' => $queryData[0],
  317. 'attributeFull' => $key,
  318. 'rangeHigh' => $high,
  319. ];
  320. return $data;
  321. }
  322. }
  323. }
  324. return [];
  325. }
  326. /**
  327. * Set password for an LDAP user identified by a DN
  328. *
  329. * @param string $userDN the user in question
  330. * @param string $password the new password
  331. * @return bool
  332. * @throws HintException
  333. * @throws \Exception
  334. */
  335. public function setPassword($userDN, $password) {
  336. if ((int)$this->connection->turnOnPasswordChange !== 1) {
  337. throw new \Exception('LDAP password changes are disabled.');
  338. }
  339. $cr = $this->connection->getConnectionResource();
  340. if (!$this->ldap->isResource($cr)) {
  341. //LDAP not available
  342. \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
  343. return false;
  344. }
  345. try {
  346. // try PASSWD extended operation first
  347. return @$this->invokeLDAPMethod('exopPasswd', $cr, $userDN, '', $password) ||
  348. @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
  349. } catch (ConstraintViolationException $e) {
  350. throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ') . $e->getMessage(), $e->getCode());
  351. }
  352. }
  353. /**
  354. * checks whether the given attributes value is probably a DN
  355. *
  356. * @param string $attr the attribute in question
  357. * @return boolean if so true, otherwise false
  358. */
  359. private function resemblesDN($attr) {
  360. $resemblingAttributes = [
  361. 'dn',
  362. 'uniquemember',
  363. 'member',
  364. // memberOf is an "operational" attribute, without a definition in any RFC
  365. 'memberof'
  366. ];
  367. return in_array($attr, $resemblingAttributes);
  368. }
  369. /**
  370. * checks whether the given string is probably a DN
  371. *
  372. * @param string $string
  373. * @return boolean
  374. */
  375. public function stringResemblesDN($string) {
  376. $r = $this->ldap->explodeDN($string, 0);
  377. // if exploding a DN succeeds and does not end up in
  378. // an empty array except for $r[count] being 0.
  379. return (is_array($r) && count($r) > 1);
  380. }
  381. /**
  382. * returns a DN-string that is cleaned from not domain parts, e.g.
  383. * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
  384. * becomes dc=foobar,dc=server,dc=org
  385. *
  386. * @param string $dn
  387. * @return string
  388. */
  389. public function getDomainDNFromDN($dn) {
  390. $allParts = $this->ldap->explodeDN($dn, 0);
  391. if ($allParts === false) {
  392. //not a valid DN
  393. return '';
  394. }
  395. $domainParts = [];
  396. $dcFound = false;
  397. foreach ($allParts as $part) {
  398. if (!$dcFound && strpos($part, 'dc=') === 0) {
  399. $dcFound = true;
  400. }
  401. if ($dcFound) {
  402. $domainParts[] = $part;
  403. }
  404. }
  405. return implode(',', $domainParts);
  406. }
  407. /**
  408. * returns the LDAP DN for the given internal Nextcloud name of the group
  409. *
  410. * @param string $name the Nextcloud name in question
  411. * @return string|false LDAP DN on success, otherwise false
  412. */
  413. public function groupname2dn($name) {
  414. return $this->groupMapper->getDNByName($name);
  415. }
  416. /**
  417. * returns the LDAP DN for the given internal Nextcloud name of the user
  418. *
  419. * @param string $name the Nextcloud name in question
  420. * @return string|false with the LDAP DN on success, otherwise false
  421. */
  422. public function username2dn($name) {
  423. $fdn = $this->userMapper->getDNByName($name);
  424. //Check whether the DN belongs to the Base, to avoid issues on multi-
  425. //server setups
  426. if (is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  427. return $fdn;
  428. }
  429. return false;
  430. }
  431. /**
  432. * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
  433. *
  434. * @param string $fdn the dn of the group object
  435. * @param string $ldapName optional, the display name of the object
  436. * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
  437. * @throws \Exception
  438. */
  439. public function dn2groupname($fdn, $ldapName = null) {
  440. //To avoid bypassing the base DN settings under certain circumstances
  441. //with the group support, check whether the provided DN matches one of
  442. //the given Bases
  443. if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
  444. return false;
  445. }
  446. return $this->dn2ocname($fdn, $ldapName, false);
  447. }
  448. /**
  449. * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  450. *
  451. * @param string $dn the dn of the user object
  452. * @param string $ldapName optional, the display name of the object
  453. * @return string|false with with the name to use in Nextcloud
  454. * @throws \Exception
  455. */
  456. public function dn2username($fdn, $ldapName = null) {
  457. //To avoid bypassing the base DN settings under certain circumstances
  458. //with the group support, check whether the provided DN matches one of
  459. //the given Bases
  460. if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  461. return false;
  462. }
  463. return $this->dn2ocname($fdn, $ldapName, true);
  464. }
  465. /**
  466. * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
  467. *
  468. * @param string $fdn the dn of the user object
  469. * @param string|null $ldapName optional, the display name of the object
  470. * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
  471. * @param bool|null $newlyMapped
  472. * @param array|null $record
  473. * @return false|string with with the name to use in Nextcloud
  474. * @throws \Exception
  475. */
  476. public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
  477. $newlyMapped = false;
  478. if ($isUser) {
  479. $mapper = $this->getUserMapper();
  480. $nameAttribute = $this->connection->ldapUserDisplayName;
  481. $filter = $this->connection->ldapUserFilter;
  482. } else {
  483. $mapper = $this->getGroupMapper();
  484. $nameAttribute = $this->connection->ldapGroupDisplayName;
  485. $filter = $this->connection->ldapGroupFilter;
  486. }
  487. //let's try to retrieve the Nextcloud name from the mappings table
  488. $ncName = $mapper->getNameByDN($fdn);
  489. if (is_string($ncName)) {
  490. return $ncName;
  491. }
  492. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  493. $uuid = $this->getUUID($fdn, $isUser, $record);
  494. if (is_string($uuid)) {
  495. $ncName = $mapper->getNameByUUID($uuid);
  496. if (is_string($ncName)) {
  497. $mapper->setDNbyUUID($fdn, $uuid);
  498. return $ncName;
  499. }
  500. } else {
  501. //If the UUID can't be detected something is foul.
  502. \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for ' . $fdn . '. Skipping.', ILogger::INFO);
  503. return false;
  504. }
  505. if (is_null($ldapName)) {
  506. $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
  507. if (!isset($ldapName[0]) && empty($ldapName[0])) {
  508. \OCP\Util::writeLog('user_ldap', 'No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ILogger::INFO);
  509. return false;
  510. }
  511. $ldapName = $ldapName[0];
  512. }
  513. if ($isUser) {
  514. $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
  515. if ($usernameAttribute !== '') {
  516. $username = $this->readAttribute($fdn, $usernameAttribute);
  517. $username = $username[0];
  518. } else {
  519. $username = $uuid;
  520. }
  521. try {
  522. $intName = $this->sanitizeUsername($username);
  523. } catch (\InvalidArgumentException $e) {
  524. \OC::$server->getLogger()->logException($e, [
  525. 'app' => 'user_ldap',
  526. 'level' => ILogger::WARN,
  527. ]);
  528. // we don't attempt to set a username here. We can go for
  529. // for an alternative 4 digit random number as we would append
  530. // otherwise, however it's likely not enough space in bigger
  531. // setups, and most importantly: this is not intended.
  532. return false;
  533. }
  534. } else {
  535. $intName = $ldapName;
  536. }
  537. //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
  538. //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
  539. //NOTE: mind, disabling cache affects only this instance! Using it
  540. // outside of core user management will still cache the user as non-existing.
  541. $originalTTL = $this->connection->ldapCacheTTL;
  542. $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
  543. if ($intName !== ''
  544. && (($isUser && !$this->ncUserManager->userExists($intName))
  545. || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
  546. )
  547. ) {
  548. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  549. $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
  550. if ($newlyMapped) {
  551. return $intName;
  552. }
  553. }
  554. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  555. $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
  556. if (is_string($altName)) {
  557. if ($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
  558. $newlyMapped = true;
  559. return $altName;
  560. }
  561. }
  562. //if everything else did not help..
  563. \OCP\Util::writeLog('user_ldap', 'Could not create unique name for ' . $fdn . '.', ILogger::INFO);
  564. return false;
  565. }
  566. public function mapAndAnnounceIfApplicable(
  567. AbstractMapping $mapper,
  568. string $fdn,
  569. string $name,
  570. string $uuid,
  571. bool $isUser
  572. ): bool {
  573. if ($mapper->map($fdn, $name, $uuid)) {
  574. if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
  575. $this->cacheUserExists($name);
  576. $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
  577. } elseif (!$isUser) {
  578. $this->cacheGroupExists($name);
  579. }
  580. return true;
  581. }
  582. return false;
  583. }
  584. /**
  585. * gives back the user names as they are used ownClod internally
  586. *
  587. * @param array $ldapUsers as returned by fetchList()
  588. * @return array an array with the user names to use in Nextcloud
  589. *
  590. * gives back the user names as they are used ownClod internally
  591. * @throws \Exception
  592. */
  593. public function nextcloudUserNames($ldapUsers) {
  594. return $this->ldap2NextcloudNames($ldapUsers, true);
  595. }
  596. /**
  597. * gives back the group names as they are used ownClod internally
  598. *
  599. * @param array $ldapGroups as returned by fetchList()
  600. * @return array an array with the group names to use in Nextcloud
  601. *
  602. * gives back the group names as they are used ownClod internally
  603. * @throws \Exception
  604. */
  605. public function nextcloudGroupNames($ldapGroups) {
  606. return $this->ldap2NextcloudNames($ldapGroups, false);
  607. }
  608. /**
  609. * @param array $ldapObjects as returned by fetchList()
  610. * @param bool $isUsers
  611. * @return array
  612. * @throws \Exception
  613. */
  614. private function ldap2NextcloudNames($ldapObjects, $isUsers) {
  615. if ($isUsers) {
  616. $nameAttribute = $this->connection->ldapUserDisplayName;
  617. $sndAttribute = $this->connection->ldapUserDisplayName2;
  618. } else {
  619. $nameAttribute = $this->connection->ldapGroupDisplayName;
  620. }
  621. $nextcloudNames = [];
  622. foreach ($ldapObjects as $ldapObject) {
  623. $nameByLDAP = null;
  624. if (isset($ldapObject[$nameAttribute])
  625. && is_array($ldapObject[$nameAttribute])
  626. && isset($ldapObject[$nameAttribute][0])
  627. ) {
  628. // might be set, but not necessarily. if so, we use it.
  629. $nameByLDAP = $ldapObject[$nameAttribute][0];
  630. }
  631. $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
  632. if ($ncName) {
  633. $nextcloudNames[] = $ncName;
  634. if ($isUsers) {
  635. $this->updateUserState($ncName);
  636. //cache the user names so it does not need to be retrieved
  637. //again later (e.g. sharing dialogue).
  638. if (is_null($nameByLDAP)) {
  639. continue;
  640. }
  641. $sndName = isset($ldapObject[$sndAttribute][0])
  642. ? $ldapObject[$sndAttribute][0] : '';
  643. $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
  644. } elseif ($nameByLDAP !== null) {
  645. $this->cacheGroupDisplayName($ncName, $nameByLDAP);
  646. }
  647. }
  648. }
  649. return $nextcloudNames;
  650. }
  651. /**
  652. * removes the deleted-flag of a user if it was set
  653. *
  654. * @param string $ncname
  655. * @throws \Exception
  656. */
  657. public function updateUserState($ncname) {
  658. $user = $this->userManager->get($ncname);
  659. if ($user instanceof OfflineUser) {
  660. $user->unmark();
  661. }
  662. }
  663. /**
  664. * caches the user display name
  665. *
  666. * @param string $ocName the internal Nextcloud username
  667. * @param string|false $home the home directory path
  668. */
  669. public function cacheUserHome($ocName, $home) {
  670. $cacheKey = 'getHome' . $ocName;
  671. $this->connection->writeToCache($cacheKey, $home);
  672. }
  673. /**
  674. * caches a user as existing
  675. *
  676. * @param string $ocName the internal Nextcloud username
  677. */
  678. public function cacheUserExists($ocName) {
  679. $this->connection->writeToCache('userExists' . $ocName, true);
  680. }
  681. /**
  682. * caches a group as existing
  683. */
  684. public function cacheGroupExists(string $gid): void {
  685. $this->connection->writeToCache('groupExists' . $gid, true);
  686. }
  687. /**
  688. * caches the user display name
  689. *
  690. * @param string $ocName the internal Nextcloud username
  691. * @param string $displayName the display name
  692. * @param string $displayName2 the second display name
  693. * @throws \Exception
  694. */
  695. public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
  696. $user = $this->userManager->get($ocName);
  697. if ($user === null) {
  698. return;
  699. }
  700. $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
  701. $cacheKeyTrunk = 'getDisplayName';
  702. $this->connection->writeToCache($cacheKeyTrunk . $ocName, $displayName);
  703. }
  704. public function cacheGroupDisplayName(string $ncName, string $displayName): void {
  705. $cacheKey = 'group_getDisplayName' . $ncName;
  706. $this->connection->writeToCache($cacheKey, $displayName);
  707. }
  708. /**
  709. * creates a unique name for internal Nextcloud use for users. Don't call it directly.
  710. *
  711. * @param string $name the display name of the object
  712. * @return string|false with with the name to use in Nextcloud or false if unsuccessful
  713. *
  714. * Instead of using this method directly, call
  715. * createAltInternalOwnCloudName($name, true)
  716. */
  717. private function _createAltInternalOwnCloudNameForUsers($name) {
  718. $attempts = 0;
  719. //while loop is just a precaution. If a name is not generated within
  720. //20 attempts, something else is very wrong. Avoids infinite loop.
  721. while ($attempts < 20) {
  722. $altName = $name . '_' . rand(1000, 9999);
  723. if (!$this->ncUserManager->userExists($altName)) {
  724. return $altName;
  725. }
  726. $attempts++;
  727. }
  728. return false;
  729. }
  730. /**
  731. * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
  732. *
  733. * @param string $name the display name of the object
  734. * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
  735. *
  736. * Instead of using this method directly, call
  737. * createAltInternalOwnCloudName($name, false)
  738. *
  739. * Group names are also used as display names, so we do a sequential
  740. * numbering, e.g. Developers_42 when there are 41 other groups called
  741. * "Developers"
  742. */
  743. private function _createAltInternalOwnCloudNameForGroups($name) {
  744. $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
  745. if (!$usedNames || count($usedNames) === 0) {
  746. $lastNo = 1; //will become name_2
  747. } else {
  748. natsort($usedNames);
  749. $lastName = array_pop($usedNames);
  750. $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
  751. }
  752. $altName = $name . '_' . (string)($lastNo + 1);
  753. unset($usedNames);
  754. $attempts = 1;
  755. while ($attempts < 21) {
  756. // Check to be really sure it is unique
  757. // while loop is just a precaution. If a name is not generated within
  758. // 20 attempts, something else is very wrong. Avoids infinite loop.
  759. if (!\OC::$server->getGroupManager()->groupExists($altName)) {
  760. return $altName;
  761. }
  762. $altName = $name . '_' . ($lastNo + $attempts);
  763. $attempts++;
  764. }
  765. return false;
  766. }
  767. /**
  768. * creates a unique name for internal Nextcloud use.
  769. *
  770. * @param string $name the display name of the object
  771. * @param boolean $isUser whether name should be created for a user (true) or a group (false)
  772. * @return string|false with with the name to use in Nextcloud or false if unsuccessful
  773. */
  774. private function createAltInternalOwnCloudName($name, $isUser) {
  775. $originalTTL = $this->connection->ldapCacheTTL;
  776. $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
  777. if ($isUser) {
  778. $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
  779. } else {
  780. $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
  781. }
  782. $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
  783. return $altName;
  784. }
  785. /**
  786. * fetches a list of users according to a provided loginName and utilizing
  787. * the login filter.
  788. */
  789. public function fetchUsersByLoginName(string $loginName, array $attributes = ['dn']): array {
  790. $loginName = $this->escapeFilterPart($loginName);
  791. $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
  792. return $this->fetchListOfUsers($filter, $attributes);
  793. }
  794. /**
  795. * counts the number of users according to a provided loginName and
  796. * utilizing the login filter.
  797. *
  798. * @param string $loginName
  799. * @return int
  800. */
  801. public function countUsersByLoginName($loginName) {
  802. $loginName = $this->escapeFilterPart($loginName);
  803. $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
  804. return $this->countUsers($filter);
  805. }
  806. /**
  807. * @throws \Exception
  808. */
  809. public function fetchListOfUsers(string $filter, array $attr, int $limit = null, int $offset = null, bool $forceApplyAttributes = false): array {
  810. $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
  811. $recordsToUpdate = $ldapRecords;
  812. if (!$forceApplyAttributes) {
  813. $isBackgroundJobModeAjax = $this->config
  814. ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
  815. $listOfDNs = array_reduce($ldapRecords, function ($listOfDNs, $entry) {
  816. $listOfDNs[] = $entry['dn'][0];
  817. return $listOfDNs;
  818. }, []);
  819. $idsByDn = $this->userMapper->getListOfIdsByDn($listOfDNs);
  820. $recordsToUpdate = array_filter($ldapRecords, function ($record) use ($isBackgroundJobModeAjax, $idsByDn) {
  821. $newlyMapped = false;
  822. $uid = $idsByDn[$record['dn'][0]] ?? null;
  823. if ($uid === null) {
  824. $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
  825. }
  826. if (is_string($uid)) {
  827. $this->cacheUserExists($uid);
  828. }
  829. return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
  830. });
  831. }
  832. $this->batchApplyUserAttributes($recordsToUpdate);
  833. return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
  834. }
  835. /**
  836. * provided with an array of LDAP user records the method will fetch the
  837. * user object and requests it to process the freshly fetched attributes and
  838. * and their values
  839. *
  840. * @param array $ldapRecords
  841. * @throws \Exception
  842. */
  843. public function batchApplyUserAttributes(array $ldapRecords) {
  844. $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
  845. foreach ($ldapRecords as $userRecord) {
  846. if (!isset($userRecord[$displayNameAttribute])) {
  847. // displayName is obligatory
  848. continue;
  849. }
  850. $ocName = $this->dn2ocname($userRecord['dn'][0], null, true);
  851. if ($ocName === false) {
  852. continue;
  853. }
  854. $this->updateUserState($ocName);
  855. $user = $this->userManager->get($ocName);
  856. if ($user !== null) {
  857. $user->processAttributes($userRecord);
  858. } else {
  859. \OC::$server->getLogger()->debug(
  860. "The ldap user manager returned null for $ocName",
  861. ['app' => 'user_ldap']
  862. );
  863. }
  864. }
  865. }
  866. /**
  867. * @param string $filter
  868. * @param string|string[] $attr
  869. * @param int $limit
  870. * @param int $offset
  871. * @return array
  872. */
  873. public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
  874. $groupRecords = $this->searchGroups($filter, $attr, $limit, $offset);
  875. $listOfDNs = array_reduce($groupRecords, function ($listOfDNs, $entry) {
  876. $listOfDNs[] = $entry['dn'][0];
  877. return $listOfDNs;
  878. }, []);
  879. $idsByDn = $this->groupMapper->getListOfIdsByDn($listOfDNs);
  880. array_walk($groupRecords, function ($record) use ($idsByDn) {
  881. $newlyMapped = false;
  882. $gid = $uidsByDn[$record['dn'][0]] ?? null;
  883. if ($gid === null) {
  884. $gid = $this->dn2ocname($record['dn'][0], null, false, $newlyMapped, $record);
  885. }
  886. if (!$newlyMapped && is_string($gid)) {
  887. $this->cacheGroupExists($gid);
  888. }
  889. });
  890. return $this->fetchList($groupRecords, $this->manyAttributes($attr));
  891. }
  892. /**
  893. * @param array $list
  894. * @param bool $manyAttributes
  895. * @return array
  896. */
  897. private function fetchList($list, $manyAttributes) {
  898. if (is_array($list)) {
  899. if ($manyAttributes) {
  900. return $list;
  901. } else {
  902. $list = array_reduce($list, function ($carry, $item) {
  903. $attribute = array_keys($item)[0];
  904. $carry[] = $item[$attribute][0];
  905. return $carry;
  906. }, []);
  907. return array_unique($list, SORT_LOCALE_STRING);
  908. }
  909. }
  910. //error cause actually, maybe throw an exception in future.
  911. return [];
  912. }
  913. /**
  914. * @throws ServerNotAvailableException
  915. */
  916. public function searchUsers(string $filter, array $attr = null, int $limit = null, int $offset = null): array {
  917. $result = [];
  918. foreach ($this->connection->ldapBaseUsers as $base) {
  919. $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
  920. }
  921. return $result;
  922. }
  923. /**
  924. * @param string $filter
  925. * @param string|string[] $attr
  926. * @param int $limit
  927. * @param int $offset
  928. * @return false|int
  929. * @throws ServerNotAvailableException
  930. */
  931. public function countUsers($filter, $attr = ['dn'], $limit = null, $offset = null) {
  932. $result = false;
  933. foreach ($this->connection->ldapBaseUsers as $base) {
  934. $count = $this->count($filter, [$base], $attr, $limit, $offset);
  935. $result = is_int($count) ? (int)$result + $count : $result;
  936. }
  937. return $result;
  938. }
  939. /**
  940. * executes an LDAP search, optimized for Groups
  941. *
  942. * @param string $filter the LDAP filter for the search
  943. * @param string|string[] $attr optional, when a certain attribute shall be filtered out
  944. * @param integer $limit
  945. * @param integer $offset
  946. * @return array with the search result
  947. *
  948. * Executes an LDAP search
  949. * @throws ServerNotAvailableException
  950. */
  951. public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
  952. $result = [];
  953. foreach ($this->connection->ldapBaseGroups as $base) {
  954. $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
  955. }
  956. return $result;
  957. }
  958. /**
  959. * returns the number of available groups
  960. *
  961. * @param string $filter the LDAP search filter
  962. * @param string[] $attr optional
  963. * @param int|null $limit
  964. * @param int|null $offset
  965. * @return int|bool
  966. * @throws ServerNotAvailableException
  967. */
  968. public function countGroups($filter, $attr = ['dn'], $limit = null, $offset = null) {
  969. $result = false;
  970. foreach ($this->connection->ldapBaseGroups as $base) {
  971. $count = $this->count($filter, [$base], $attr, $limit, $offset);
  972. $result = is_int($count) ? (int)$result + $count : $result;
  973. }
  974. return $result;
  975. }
  976. /**
  977. * returns the number of available objects on the base DN
  978. *
  979. * @param int|null $limit
  980. * @param int|null $offset
  981. * @return int|bool
  982. * @throws ServerNotAvailableException
  983. */
  984. public function countObjects($limit = null, $offset = null) {
  985. $result = false;
  986. foreach ($this->connection->ldapBase as $base) {
  987. $count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
  988. $result = is_int($count) ? (int)$result + $count : $result;
  989. }
  990. return $result;
  991. }
  992. /**
  993. * Returns the LDAP handler
  994. *
  995. * @throws \OC\ServerNotAvailableException
  996. */
  997. /**
  998. * @return mixed
  999. * @throws \OC\ServerNotAvailableException
  1000. */
  1001. private function invokeLDAPMethod() {
  1002. $arguments = func_get_args();
  1003. $command = array_shift($arguments);
  1004. $cr = array_shift($arguments);
  1005. if (!method_exists($this->ldap, $command)) {
  1006. return null;
  1007. }
  1008. array_unshift($arguments, $cr);
  1009. // php no longer supports call-time pass-by-reference
  1010. // thus cannot support controlPagedResultResponse as the third argument
  1011. // is a reference
  1012. $doMethod = function () use ($command, &$arguments) {
  1013. if ($command == 'controlPagedResultResponse') {
  1014. throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
  1015. } else {
  1016. return call_user_func_array([$this->ldap, $command], $arguments);
  1017. }
  1018. };
  1019. try {
  1020. $ret = $doMethod();
  1021. } catch (ServerNotAvailableException $e) {
  1022. /* Server connection lost, attempt to reestablish it
  1023. * Maybe implement exponential backoff?
  1024. * This was enough to get solr indexer working which has large delays between LDAP fetches.
  1025. */
  1026. \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
  1027. $this->connection->resetConnectionResource();
  1028. $cr = $this->connection->getConnectionResource();
  1029. if (!$this->ldap->isResource($cr)) {
  1030. // Seems like we didn't find any resource.
  1031. \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
  1032. throw $e;
  1033. }
  1034. $arguments[0] = $cr;
  1035. $ret = $doMethod();
  1036. }
  1037. return $ret;
  1038. }
  1039. /**
  1040. * retrieved. Results will according to the order in the array.
  1041. *
  1042. * @param string $filter
  1043. * @param string $base
  1044. * @param string[] $attr
  1045. * @param int|null $limit optional, maximum results to be counted
  1046. * @param int|null $offset optional, a starting point
  1047. * @return array|false array with the search result as first value and pagedSearchOK as
  1048. * second | false if not successful
  1049. * @throws ServerNotAvailableException
  1050. */
  1051. private function executeSearch(
  1052. string $filter,
  1053. string $base,
  1054. ?array &$attr,
  1055. ?int $limit,
  1056. ?int $offset
  1057. ) {
  1058. // See if we have a resource, in case not cancel with message
  1059. $cr = $this->connection->getConnectionResource();
  1060. if (!$this->ldap->isResource($cr)) {
  1061. // Seems like we didn't find any resource.
  1062. // Return an empty array just like before.
  1063. \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
  1064. return false;
  1065. }
  1066. //check whether paged search should be attempted
  1067. $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, (int)$offset);
  1068. $sr = $this->invokeLDAPMethod('search', $cr, $base, $filter, $attr);
  1069. // cannot use $cr anymore, might have changed in the previous call!
  1070. $error = $this->ldap->errno($this->connection->getConnectionResource());
  1071. if (!$this->ldap->isResource($sr) || $error !== 0) {
  1072. \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? ' . print_r($pagedSearchOK, true), ILogger::ERROR);
  1073. return false;
  1074. }
  1075. return [$sr, $pagedSearchOK];
  1076. }
  1077. /**
  1078. * processes an LDAP paged search operation
  1079. *
  1080. * @param resource $sr the array containing the LDAP search resources
  1081. * @param int $foundItems number of results in the single search operation
  1082. * @param int $limit maximum results to be counted
  1083. * @param bool $pagedSearchOK whether a paged search has been executed
  1084. * @param bool $skipHandling required for paged search when cookies to
  1085. * prior results need to be gained
  1086. * @return bool cookie validity, true if we have more pages, false otherwise.
  1087. * @throws ServerNotAvailableException
  1088. */
  1089. private function processPagedSearchStatus(
  1090. $sr,
  1091. int $foundItems,
  1092. int $limit,
  1093. bool $pagedSearchOK,
  1094. bool $skipHandling
  1095. ): bool {
  1096. $cookie = null;
  1097. if ($pagedSearchOK) {
  1098. $cr = $this->connection->getConnectionResource();
  1099. if ($this->ldap->controlPagedResultResponse($cr, $sr, $cookie)) {
  1100. $this->lastCookie = $cookie;
  1101. }
  1102. //browsing through prior pages to get the cookie for the new one
  1103. if ($skipHandling) {
  1104. return false;
  1105. }
  1106. // if count is bigger, then the server does not support
  1107. // paged search. Instead, he did a normal search. We set a
  1108. // flag here, so the callee knows how to deal with it.
  1109. if ($foundItems <= $limit) {
  1110. $this->pagedSearchedSuccessful = true;
  1111. }
  1112. } else {
  1113. if (!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
  1114. \OC::$server->getLogger()->debug(
  1115. 'Paged search was not available',
  1116. ['app' => 'user_ldap']
  1117. );
  1118. }
  1119. }
  1120. /* ++ Fixing RHDS searches with pages with zero results ++
  1121. * Return cookie status. If we don't have more pages, with RHDS
  1122. * cookie is null, with openldap cookie is an empty string and
  1123. * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
  1124. */
  1125. return !empty($cookie) || $cookie === '0';
  1126. }
  1127. /**
  1128. * executes an LDAP search, but counts the results only
  1129. *
  1130. * @param string $filter the LDAP filter for the search
  1131. * @param array $bases an array containing the LDAP subtree(s) that shall be searched
  1132. * @param string|string[] $attr optional, array, one or more attributes that shall be
  1133. * retrieved. Results will according to the order in the array.
  1134. * @param int $limit optional, maximum results to be counted
  1135. * @param int $offset optional, a starting point
  1136. * @param bool $skipHandling indicates whether the pages search operation is
  1137. * completed
  1138. * @return int|false Integer or false if the search could not be initialized
  1139. * @throws ServerNotAvailableException
  1140. */
  1141. private function count(
  1142. string $filter,
  1143. array $bases,
  1144. $attr = null,
  1145. ?int $limit = null,
  1146. ?int $offset = null,
  1147. bool $skipHandling = false
  1148. ) {
  1149. \OC::$server->getLogger()->debug('Count filter: {filter}', [
  1150. 'app' => 'user_ldap',
  1151. 'filter' => $filter
  1152. ]);
  1153. if (!is_null($attr) && !is_array($attr)) {
  1154. $attr = [mb_strtolower($attr, 'UTF-8')];
  1155. }
  1156. $limitPerPage = (int)$this->connection->ldapPagingSize;
  1157. if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
  1158. $limitPerPage = $limit;
  1159. }
  1160. $counter = 0;
  1161. $count = null;
  1162. $this->connection->getConnectionResource();
  1163. foreach ($bases as $base) {
  1164. do {
  1165. $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
  1166. if ($search === false) {
  1167. return $counter > 0 ? $counter : false;
  1168. }
  1169. list($sr, $pagedSearchOK) = $search;
  1170. /* ++ Fixing RHDS searches with pages with zero results ++
  1171. * countEntriesInSearchResults() method signature changed
  1172. * by removing $limit and &$hasHitLimit parameters
  1173. */
  1174. $count = $this->countEntriesInSearchResults($sr);
  1175. $counter += $count;
  1176. $hasMorePages = $this->processPagedSearchStatus($sr, $count, $limitPerPage, $pagedSearchOK, $skipHandling);
  1177. $offset += $limitPerPage;
  1178. /* ++ Fixing RHDS searches with pages with zero results ++
  1179. * Continue now depends on $hasMorePages value
  1180. */
  1181. $continue = $pagedSearchOK && $hasMorePages;
  1182. } while ($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
  1183. }
  1184. return $counter;
  1185. }
  1186. /**
  1187. * @param resource $sr
  1188. * @return int
  1189. * @throws ServerNotAvailableException
  1190. */
  1191. private function countEntriesInSearchResults($sr): int {
  1192. return (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $sr);
  1193. }
  1194. /**
  1195. * Executes an LDAP search
  1196. *
  1197. * @throws ServerNotAvailableException
  1198. */
  1199. public function search(
  1200. string $filter,
  1201. string $base,
  1202. ?array $attr = null,
  1203. ?int $limit = null,
  1204. ?int $offset = null,
  1205. bool $skipHandling = false
  1206. ): array {
  1207. $limitPerPage = (int)$this->connection->ldapPagingSize;
  1208. if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
  1209. $limitPerPage = $limit;
  1210. }
  1211. if (!is_null($attr) && !is_array($attr)) {
  1212. $attr = [mb_strtolower($attr, 'UTF-8')];
  1213. }
  1214. /* ++ Fixing RHDS searches with pages with zero results ++
  1215. * As we can have pages with zero results and/or pages with less
  1216. * than $limit results but with a still valid server 'cookie',
  1217. * loops through until we get $continue equals true and
  1218. * $findings['count'] < $limit
  1219. */
  1220. $findings = [];
  1221. $savedoffset = $offset;
  1222. $iFoundItems = 0;
  1223. do {
  1224. $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
  1225. if ($search === false) {
  1226. return [];
  1227. }
  1228. list($sr, $pagedSearchOK) = $search;
  1229. $cr = $this->connection->getConnectionResource();
  1230. if ($skipHandling) {
  1231. //i.e. result do not need to be fetched, we just need the cookie
  1232. //thus pass 1 or any other value as $iFoundItems because it is not
  1233. //used
  1234. $this->processPagedSearchStatus($sr, 1, $limitPerPage, $pagedSearchOK, $skipHandling);
  1235. return [];
  1236. }
  1237. $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $sr));
  1238. $iFoundItems = max($iFoundItems, $findings['count']);
  1239. unset($findings['count']);
  1240. $continue = $this->processPagedSearchStatus($sr, $iFoundItems, $limitPerPage, $pagedSearchOK, $skipHandling);
  1241. $offset += $limitPerPage;
  1242. } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
  1243. // resetting offset
  1244. $offset = $savedoffset;
  1245. // if we're here, probably no connection resource is returned.
  1246. // to make Nextcloud behave nicely, we simply give back an empty array.
  1247. if (is_null($findings)) {
  1248. return [];
  1249. }
  1250. if (!is_null($attr)) {
  1251. $selection = [];
  1252. $i = 0;
  1253. foreach ($findings as $item) {
  1254. if (!is_array($item)) {
  1255. continue;
  1256. }
  1257. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  1258. foreach ($attr as $key) {
  1259. if (isset($item[$key])) {
  1260. if (is_array($item[$key]) && isset($item[$key]['count'])) {
  1261. unset($item[$key]['count']);
  1262. }
  1263. if ($key !== 'dn') {
  1264. if ($this->resemblesDN($key)) {
  1265. $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
  1266. } elseif ($key === 'objectguid' || $key === 'guid') {
  1267. $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
  1268. } else {
  1269. $selection[$i][$key] = $item[$key];
  1270. }
  1271. } else {
  1272. $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
  1273. }
  1274. }
  1275. }
  1276. $i++;
  1277. }
  1278. $findings = $selection;
  1279. }
  1280. //we slice the findings, when
  1281. //a) paged search unsuccessful, though attempted
  1282. //b) no paged search, but limit set
  1283. if ((!$this->getPagedSearchResultState()
  1284. && $pagedSearchOK)
  1285. || (
  1286. !$pagedSearchOK
  1287. && !is_null($limit)
  1288. )
  1289. ) {
  1290. $findings = array_slice($findings, (int)$offset, $limit);
  1291. }
  1292. return $findings;
  1293. }
  1294. /**
  1295. * @param string $name
  1296. * @return string
  1297. * @throws \InvalidArgumentException
  1298. */
  1299. public function sanitizeUsername($name) {
  1300. $name = trim($name);
  1301. if ($this->connection->ldapIgnoreNamingRules) {
  1302. return $name;
  1303. }
  1304. // Transliteration to ASCII
  1305. $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
  1306. if ($transliterated !== false) {
  1307. // depending on system config iconv can work or not
  1308. $name = $transliterated;
  1309. }
  1310. // Replacements
  1311. $name = str_replace(' ', '_', $name);
  1312. // Every remaining disallowed characters will be removed
  1313. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  1314. if ($name === '') {
  1315. throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
  1316. }
  1317. return $name;
  1318. }
  1319. /**
  1320. * escapes (user provided) parts for LDAP filter
  1321. *
  1322. * @param string $input , the provided value
  1323. * @param bool $allowAsterisk whether in * at the beginning should be preserved
  1324. * @return string the escaped string
  1325. */
  1326. public function escapeFilterPart($input, $allowAsterisk = false): string {
  1327. $asterisk = '';
  1328. if ($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
  1329. $asterisk = '*';
  1330. $input = mb_substr($input, 1, null, 'UTF-8');
  1331. }
  1332. $search = ['*', '\\', '(', ')'];
  1333. $replace = ['\\*', '\\\\', '\\(', '\\)'];
  1334. return $asterisk . str_replace($search, $replace, $input);
  1335. }
  1336. /**
  1337. * combines the input filters with AND
  1338. *
  1339. * @param string[] $filters the filters to connect
  1340. * @return string the combined filter
  1341. */
  1342. public function combineFilterWithAnd($filters): string {
  1343. return $this->combineFilter($filters, '&');
  1344. }
  1345. /**
  1346. * combines the input filters with OR
  1347. *
  1348. * @param string[] $filters the filters to connect
  1349. * @return string the combined filter
  1350. * Combines Filter arguments with OR
  1351. */
  1352. public function combineFilterWithOr($filters) {
  1353. return $this->combineFilter($filters, '|');
  1354. }
  1355. /**
  1356. * combines the input filters with given operator
  1357. *
  1358. * @param string[] $filters the filters to connect
  1359. * @param string $operator either & or |
  1360. * @return string the combined filter
  1361. */
  1362. private function combineFilter($filters, $operator) {
  1363. $combinedFilter = '(' . $operator;
  1364. foreach ($filters as $filter) {
  1365. if ($filter !== '' && $filter[0] !== '(') {
  1366. $filter = '(' . $filter . ')';
  1367. }
  1368. $combinedFilter .= $filter;
  1369. }
  1370. $combinedFilter .= ')';
  1371. return $combinedFilter;
  1372. }
  1373. /**
  1374. * creates a filter part for to perform search for users
  1375. *
  1376. * @param string $search the search term
  1377. * @return string the final filter part to use in LDAP searches
  1378. */
  1379. public function getFilterPartForUserSearch($search) {
  1380. return $this->getFilterPartForSearch($search,
  1381. $this->connection->ldapAttributesForUserSearch,
  1382. $this->connection->ldapUserDisplayName);
  1383. }
  1384. /**
  1385. * creates a filter part for to perform search for groups
  1386. *
  1387. * @param string $search the search term
  1388. * @return string the final filter part to use in LDAP searches
  1389. */
  1390. public function getFilterPartForGroupSearch($search) {
  1391. return $this->getFilterPartForSearch($search,
  1392. $this->connection->ldapAttributesForGroupSearch,
  1393. $this->connection->ldapGroupDisplayName);
  1394. }
  1395. /**
  1396. * creates a filter part for searches by splitting up the given search
  1397. * string into single words
  1398. *
  1399. * @param string $search the search term
  1400. * @param string[] $searchAttributes needs to have at least two attributes,
  1401. * otherwise it does not make sense :)
  1402. * @return string the final filter part to use in LDAP searches
  1403. * @throws \Exception
  1404. */
  1405. private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
  1406. if (!is_array($searchAttributes) || count($searchAttributes) < 2) {
  1407. throw new \Exception('searchAttributes must be an array with at least two string');
  1408. }
  1409. $searchWords = explode(' ', trim($search));
  1410. $wordFilters = [];
  1411. foreach ($searchWords as $word) {
  1412. $word = $this->prepareSearchTerm($word);
  1413. //every word needs to appear at least once
  1414. $wordMatchOneAttrFilters = [];
  1415. foreach ($searchAttributes as $attr) {
  1416. $wordMatchOneAttrFilters[] = $attr . '=' . $word;
  1417. }
  1418. $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
  1419. }
  1420. return $this->combineFilterWithAnd($wordFilters);
  1421. }
  1422. /**
  1423. * creates a filter part for searches
  1424. *
  1425. * @param string $search the search term
  1426. * @param string[]|null $searchAttributes
  1427. * @param string $fallbackAttribute a fallback attribute in case the user
  1428. * did not define search attributes. Typically the display name attribute.
  1429. * @return string the final filter part to use in LDAP searches
  1430. */
  1431. private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
  1432. $filter = [];
  1433. $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
  1434. if ($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
  1435. try {
  1436. return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
  1437. } catch (\Exception $e) {
  1438. \OCP\Util::writeLog(
  1439. 'user_ldap',
  1440. 'Creating advanced filter for search failed, falling back to simple method.',
  1441. ILogger::INFO
  1442. );
  1443. }
  1444. }
  1445. $search = $this->prepareSearchTerm($search);
  1446. if (!is_array($searchAttributes) || count($searchAttributes) === 0) {
  1447. if ($fallbackAttribute === '') {
  1448. return '';
  1449. }
  1450. $filter[] = $fallbackAttribute . '=' . $search;
  1451. } else {
  1452. foreach ($searchAttributes as $attribute) {
  1453. $filter[] = $attribute . '=' . $search;
  1454. }
  1455. }
  1456. if (count($filter) === 1) {
  1457. return '(' . $filter[0] . ')';
  1458. }
  1459. return $this->combineFilterWithOr($filter);
  1460. }
  1461. /**
  1462. * returns the search term depending on whether we are allowed
  1463. * list users found by ldap with the current input appended by
  1464. * a *
  1465. *
  1466. * @return string
  1467. */
  1468. private function prepareSearchTerm($term) {
  1469. $config = \OC::$server->getConfig();
  1470. $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
  1471. $result = $term;
  1472. if ($term === '') {
  1473. $result = '*';
  1474. } elseif ($allowEnum !== 'no') {
  1475. $result = $term . '*';
  1476. }
  1477. return $result;
  1478. }
  1479. /**
  1480. * returns the filter used for counting users
  1481. *
  1482. * @return string
  1483. */
  1484. public function getFilterForUserCount() {
  1485. $filter = $this->combineFilterWithAnd([
  1486. $this->connection->ldapUserFilter,
  1487. $this->connection->ldapUserDisplayName . '=*'
  1488. ]);
  1489. return $filter;
  1490. }
  1491. /**
  1492. * @param string $name
  1493. * @param string $password
  1494. * @return bool
  1495. */
  1496. public function areCredentialsValid($name, $password) {
  1497. $name = $this->helper->DNasBaseParameter($name);
  1498. $testConnection = clone $this->connection;
  1499. $credentials = [
  1500. 'ldapAgentName' => $name,
  1501. 'ldapAgentPassword' => $password
  1502. ];
  1503. if (!$testConnection->setConfiguration($credentials)) {
  1504. return false;
  1505. }
  1506. return $testConnection->bind();
  1507. }
  1508. /**
  1509. * reverse lookup of a DN given a known UUID
  1510. *
  1511. * @param string $uuid
  1512. * @return string
  1513. * @throws \Exception
  1514. */
  1515. public function getUserDnByUuid($uuid) {
  1516. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1517. $filter = $this->connection->ldapUserFilter;
  1518. $bases = $this->connection->ldapBaseUsers;
  1519. if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
  1520. // Sacrebleu! The UUID attribute is unknown :( We need first an
  1521. // existing DN to be able to reliably detect it.
  1522. foreach ($bases as $base) {
  1523. $result = $this->search($filter, $base, ['dn'], 1);
  1524. if (!isset($result[0]) || !isset($result[0]['dn'])) {
  1525. continue;
  1526. }
  1527. $dn = $result[0]['dn'][0];
  1528. if ($hasFound = $this->detectUuidAttribute($dn, true)) {
  1529. break;
  1530. }
  1531. }
  1532. if (!isset($hasFound) || !$hasFound) {
  1533. throw new \Exception('Cannot determine UUID attribute');
  1534. }
  1535. } else {
  1536. // The UUID attribute is either known or an override is given.
  1537. // By calling this method we ensure that $this->connection->$uuidAttr
  1538. // is definitely set
  1539. if (!$this->detectUuidAttribute('', true)) {
  1540. throw new \Exception('Cannot determine UUID attribute');
  1541. }
  1542. }
  1543. $uuidAttr = $this->connection->ldapUuidUserAttribute;
  1544. if ($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
  1545. $uuid = $this->formatGuid2ForFilterUser($uuid);
  1546. }
  1547. $filter = $uuidAttr . '=' . $uuid;
  1548. $result = $this->searchUsers($filter, ['dn'], 2);
  1549. if (is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
  1550. // we put the count into account to make sure that this is
  1551. // really unique
  1552. return $result[0]['dn'][0];
  1553. }
  1554. throw new \Exception('Cannot determine UUID attribute');
  1555. }
  1556. /**
  1557. * auto-detects the directory's UUID attribute
  1558. *
  1559. * @param string $dn a known DN used to check against
  1560. * @param bool $isUser
  1561. * @param bool $force the detection should be run, even if it is not set to auto
  1562. * @param array|null $ldapRecord
  1563. * @return bool true on success, false otherwise
  1564. * @throws ServerNotAvailableException
  1565. */
  1566. private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
  1567. if ($isUser) {
  1568. $uuidAttr = 'ldapUuidUserAttribute';
  1569. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1570. } else {
  1571. $uuidAttr = 'ldapUuidGroupAttribute';
  1572. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1573. }
  1574. if (!$force) {
  1575. if ($this->connection->$uuidAttr !== 'auto') {
  1576. return true;
  1577. } elseif (is_string($uuidOverride) && trim($uuidOverride) !== '') {
  1578. $this->connection->$uuidAttr = $uuidOverride;
  1579. return true;
  1580. }
  1581. $attribute = $this->connection->getFromCache($uuidAttr);
  1582. if (!$attribute === null) {
  1583. $this->connection->$uuidAttr = $attribute;
  1584. return true;
  1585. }
  1586. }
  1587. foreach (self::UUID_ATTRIBUTES as $attribute) {
  1588. if ($ldapRecord !== null) {
  1589. // we have the info from LDAP already, we don't need to talk to the server again
  1590. if (isset($ldapRecord[$attribute])) {
  1591. $this->connection->$uuidAttr = $attribute;
  1592. return true;
  1593. }
  1594. }
  1595. $value = $this->readAttribute($dn, $attribute);
  1596. if (is_array($value) && isset($value[0]) && !empty($value[0])) {
  1597. \OC::$server->getLogger()->debug(
  1598. 'Setting {attribute} as {subject}',
  1599. [
  1600. 'app' => 'user_ldap',
  1601. 'attribute' => $attribute,
  1602. 'subject' => $uuidAttr
  1603. ]
  1604. );
  1605. $this->connection->$uuidAttr = $attribute;
  1606. $this->connection->writeToCache($uuidAttr, $attribute);
  1607. return true;
  1608. }
  1609. }
  1610. \OC::$server->getLogger()->debug('Could not autodetect the UUID attribute', ['app' => 'user_ldap']);
  1611. return false;
  1612. }
  1613. /**
  1614. * @param string $dn
  1615. * @param bool $isUser
  1616. * @param null $ldapRecord
  1617. * @return bool|string
  1618. * @throws ServerNotAvailableException
  1619. */
  1620. public function getUUID($dn, $isUser = true, $ldapRecord = null) {
  1621. if ($isUser) {
  1622. $uuidAttr = 'ldapUuidUserAttribute';
  1623. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1624. } else {
  1625. $uuidAttr = 'ldapUuidGroupAttribute';
  1626. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1627. }
  1628. $uuid = false;
  1629. if ($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
  1630. $attr = $this->connection->$uuidAttr;
  1631. $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
  1632. if (!is_array($uuid)
  1633. && $uuidOverride !== ''
  1634. && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) {
  1635. $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
  1636. ? $ldapRecord[$this->connection->$uuidAttr]
  1637. : $this->readAttribute($dn, $this->connection->$uuidAttr);
  1638. }
  1639. if (is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
  1640. $uuid = $uuid[0];
  1641. }
  1642. }
  1643. return $uuid;
  1644. }
  1645. /**
  1646. * converts a binary ObjectGUID into a string representation
  1647. *
  1648. * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
  1649. * @return string
  1650. * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  1651. */
  1652. private function convertObjectGUID2Str($oguid) {
  1653. $hex_guid = bin2hex($oguid);
  1654. $hex_guid_to_guid_str = '';
  1655. for ($k = 1; $k <= 4; ++$k) {
  1656. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  1657. }
  1658. $hex_guid_to_guid_str .= '-';
  1659. for ($k = 1; $k <= 2; ++$k) {
  1660. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  1661. }
  1662. $hex_guid_to_guid_str .= '-';
  1663. for ($k = 1; $k <= 2; ++$k) {
  1664. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  1665. }
  1666. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  1667. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  1668. return strtoupper($hex_guid_to_guid_str);
  1669. }
  1670. /**
  1671. * the first three blocks of the string-converted GUID happen to be in
  1672. * reverse order. In order to use it in a filter, this needs to be
  1673. * corrected. Furthermore the dashes need to be replaced and \\ preprended
  1674. * to every two hax figures.
  1675. *
  1676. * If an invalid string is passed, it will be returned without change.
  1677. *
  1678. * @param string $guid
  1679. * @return string
  1680. */
  1681. public function formatGuid2ForFilterUser($guid) {
  1682. if (!is_string($guid)) {
  1683. throw new \InvalidArgumentException('String expected');
  1684. }
  1685. $blocks = explode('-', $guid);
  1686. if (count($blocks) !== 5) {
  1687. /*
  1688. * Why not throw an Exception instead? This method is a utility
  1689. * called only when trying to figure out whether a "missing" known
  1690. * LDAP user was or was not renamed on the LDAP server. And this
  1691. * even on the use case that a reverse lookup is needed (UUID known,
  1692. * not DN), i.e. when finding users (search dialog, users page,
  1693. * login, ) this will not be fired. This occurs only if shares from
  1694. * a users are supposed to be mounted who cannot be found. Throwing
  1695. * an exception here would kill the experience for a valid, acting
  1696. * user. Instead we write a log message.
  1697. */
  1698. \OC::$server->getLogger()->info(
  1699. 'Passed string does not resemble a valid GUID. Known UUID ' .
  1700. '({uuid}) probably does not match UUID configuration.',
  1701. ['app' => 'user_ldap', 'uuid' => $guid]
  1702. );
  1703. return $guid;
  1704. }
  1705. for ($i = 0; $i < 3; $i++) {
  1706. $pairs = str_split($blocks[$i], 2);
  1707. $pairs = array_reverse($pairs);
  1708. $blocks[$i] = implode('', $pairs);
  1709. }
  1710. for ($i = 0; $i < 5; $i++) {
  1711. $pairs = str_split($blocks[$i], 2);
  1712. $blocks[$i] = '\\' . implode('\\', $pairs);
  1713. }
  1714. return implode('', $blocks);
  1715. }
  1716. /**
  1717. * gets a SID of the domain of the given dn
  1718. *
  1719. * @param string $dn
  1720. * @return string|bool
  1721. * @throws ServerNotAvailableException
  1722. */
  1723. public function getSID($dn) {
  1724. $domainDN = $this->getDomainDNFromDN($dn);
  1725. $cacheKey = 'getSID-' . $domainDN;
  1726. $sid = $this->connection->getFromCache($cacheKey);
  1727. if (!is_null($sid)) {
  1728. return $sid;
  1729. }
  1730. $objectSid = $this->readAttribute($domainDN, 'objectsid');
  1731. if (!is_array($objectSid) || empty($objectSid)) {
  1732. $this->connection->writeToCache($cacheKey, false);
  1733. return false;
  1734. }
  1735. $domainObjectSid = $this->convertSID2Str($objectSid[0]);
  1736. $this->connection->writeToCache($cacheKey, $domainObjectSid);
  1737. return $domainObjectSid;
  1738. }
  1739. /**
  1740. * converts a binary SID into a string representation
  1741. *
  1742. * @param string $sid
  1743. * @return string
  1744. */
  1745. public function convertSID2Str($sid) {
  1746. // The format of a SID binary string is as follows:
  1747. // 1 byte for the revision level
  1748. // 1 byte for the number n of variable sub-ids
  1749. // 6 bytes for identifier authority value
  1750. // n*4 bytes for n sub-ids
  1751. //
  1752. // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
  1753. // Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
  1754. $revision = ord($sid[0]);
  1755. $numberSubID = ord($sid[1]);
  1756. $subIdStart = 8; // 1 + 1 + 6
  1757. $subIdLength = 4;
  1758. if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
  1759. // Incorrect number of bytes present.
  1760. return '';
  1761. }
  1762. // 6 bytes = 48 bits can be represented using floats without loss of
  1763. // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
  1764. $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
  1765. $subIDs = [];
  1766. for ($i = 0; $i < $numberSubID; $i++) {
  1767. $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
  1768. $subIDs[] = sprintf('%u', $subID[1]);
  1769. }
  1770. // Result for example above: S-1-5-21-249921958-728525901-1594176202
  1771. return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
  1772. }
  1773. /**
  1774. * checks if the given DN is part of the given base DN(s)
  1775. *
  1776. * @param string $dn the DN
  1777. * @param string[] $bases array containing the allowed base DN or DNs
  1778. * @return bool
  1779. */
  1780. public function isDNPartOfBase($dn, $bases) {
  1781. $belongsToBase = false;
  1782. $bases = $this->helper->sanitizeDN($bases);
  1783. foreach ($bases as $base) {
  1784. $belongsToBase = true;
  1785. if (mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8') - mb_strlen($base, 'UTF-8'))) {
  1786. $belongsToBase = false;
  1787. }
  1788. if ($belongsToBase) {
  1789. break;
  1790. }
  1791. }
  1792. return $belongsToBase;
  1793. }
  1794. /**
  1795. * resets a running Paged Search operation
  1796. *
  1797. * @throws ServerNotAvailableException
  1798. */
  1799. private function abandonPagedSearch() {
  1800. if ($this->lastCookie === '') {
  1801. return;
  1802. }
  1803. $cr = $this->connection->getConnectionResource();
  1804. $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false);
  1805. $this->getPagedSearchResultState();
  1806. $this->lastCookie = '';
  1807. }
  1808. /**
  1809. * checks whether an LDAP paged search operation has more pages that can be
  1810. * retrieved, typically when offset and limit are provided.
  1811. *
  1812. * Be very careful to use it: the last cookie value, which is inspected, can
  1813. * be reset by other operations. Best, call it immediately after a search(),
  1814. * searchUsers() or searchGroups() call. count-methods are probably safe as
  1815. * well. Don't rely on it with any fetchList-method.
  1816. *
  1817. * @return bool
  1818. */
  1819. public function hasMoreResults() {
  1820. if (empty($this->lastCookie) && $this->lastCookie !== '0') {
  1821. // as in RFC 2696, when all results are returned, the cookie will
  1822. // be empty.
  1823. return false;
  1824. }
  1825. return true;
  1826. }
  1827. /**
  1828. * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  1829. *
  1830. * @return boolean|null true on success, null or false otherwise
  1831. */
  1832. public function getPagedSearchResultState() {
  1833. $result = $this->pagedSearchedSuccessful;
  1834. $this->pagedSearchedSuccessful = null;
  1835. return $result;
  1836. }
  1837. /**
  1838. * Prepares a paged search, if possible
  1839. *
  1840. * @param string $filter the LDAP filter for the search
  1841. * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
  1842. * @param string[] $attr optional, when a certain attribute shall be filtered outside
  1843. * @param int $limit
  1844. * @param int $offset
  1845. * @return bool|true
  1846. * @throws ServerNotAvailableException
  1847. */
  1848. private function initPagedSearch(
  1849. string $filter,
  1850. string $base,
  1851. ?array $attr,
  1852. int $limit,
  1853. int $offset
  1854. ): bool {
  1855. $pagedSearchOK = false;
  1856. if ($limit !== 0) {
  1857. \OC::$server->getLogger()->debug(
  1858. 'initializing paged search for filter {filter}, base {base}, attr {attr}, limit {limit}, offset {offset}',
  1859. [
  1860. 'app' => 'user_ldap',
  1861. 'filter' => $filter,
  1862. 'base' => $base,
  1863. 'attr' => $attr,
  1864. 'limit' => $limit,
  1865. 'offset' => $offset
  1866. ]
  1867. );
  1868. //get the cookie from the search for the previous search, required by LDAP
  1869. if (empty($this->lastCookie) && $this->lastCookie !== "0" && ($offset > 0)) {
  1870. // no cookie known from a potential previous search. We need
  1871. // to start from 0 to come to the desired page. cookie value
  1872. // of '0' is valid, because 389ds
  1873. $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
  1874. $this->search($filter, $base, $attr, $limit, $reOffset, true);
  1875. if (!$this->hasMoreResults()) {
  1876. // when the cookie is reset with != 0 offset, there are no further
  1877. // results, so stop.
  1878. return false;
  1879. }
  1880. }
  1881. if ($this->lastCookie !== '' && $offset === 0) {
  1882. //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
  1883. $this->abandonPagedSearch();
  1884. }
  1885. $pagedSearchOK = true === $this->invokeLDAPMethod(
  1886. 'controlPagedResult', $this->connection->getConnectionResource(), $limit, false
  1887. );
  1888. if ($pagedSearchOK) {
  1889. \OC::$server->getLogger()->debug('Ready for a paged search', ['app' => 'user_ldap']);
  1890. }
  1891. /* ++ Fixing RHDS searches with pages with zero results ++
  1892. * We coudn't get paged searches working with our RHDS for login ($limit = 0),
  1893. * due to pages with zero results.
  1894. * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
  1895. * if we don't have a previous paged search.
  1896. */
  1897. } elseif ($limit === 0 && !empty($this->lastCookie)) {
  1898. // a search without limit was requested. However, if we do use
  1899. // Paged Search once, we always must do it. This requires us to
  1900. // initialize it with the configured page size.
  1901. $this->abandonPagedSearch();
  1902. // in case someone set it to 0 … use 500, otherwise no results will
  1903. // be returned.
  1904. $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
  1905. $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
  1906. $this->connection->getConnectionResource(),
  1907. $pageSize, false);
  1908. }
  1909. return $pagedSearchOK;
  1910. }
  1911. /**
  1912. * Is more than one $attr used for search?
  1913. *
  1914. * @param string|string[]|null $attr
  1915. * @return bool
  1916. */
  1917. private function manyAttributes($attr): bool {
  1918. if (\is_array($attr)) {
  1919. return \count($attr) > 1;
  1920. }
  1921. return false;
  1922. }
  1923. }