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.

1302 lines
39 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
14 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Alexander Bergolth <leo@strike.wu.ac.at>
  6. * @author Alex Weirig <alex.weirig@technolink.lu>
  7. * @author alexweirig <alex.weirig@technolink.lu>
  8. * @author Andreas Fischer <bantu@owncloud.com>
  9. * @author Andreas Pflug <dev@admin4.org>
  10. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  11. * @author Bart Visscher <bartv@thisnet.nl>
  12. * @author Christopher Schäpers <kondou@ts.unde.re>
  13. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  14. * @author Frédéric Fortier <frederic.fortier@oronospolytechnique.com>
  15. * @author Joas Schilling <coding@schilljs.com>
  16. * @author Lukas Reschke <lukas@statuscode.ch>
  17. * @author Morris Jobke <hey@morrisjobke.de>
  18. * @author Nicolas Grekas <nicolas.grekas@gmail.com>
  19. * @author Robin McCorkell <robin@mccorkell.me.uk>
  20. * @author Roeland Jago Douma <roeland@famdouma.nl>
  21. * @author Roland Tapken <roland@bitarbeiter.net>
  22. * @author Thomas Müller <thomas.mueller@tmit.eu>
  23. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  24. * @author Vincent Petry <pvince81@owncloud.com>
  25. * @author Vinicius Cubas Brand <vinicius@eita.org.br>
  26. * @author Xuanwo <xuanwo@yunify.com>
  27. *
  28. * @license AGPL-3.0
  29. *
  30. * This code is free software: you can redistribute it and/or modify
  31. * it under the terms of the GNU Affero General Public License, version 3,
  32. * as published by the Free Software Foundation.
  33. *
  34. * This program is distributed in the hope that it will be useful,
  35. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  36. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  37. * GNU Affero General Public License for more details.
  38. *
  39. * You should have received a copy of the GNU Affero General Public License, version 3,
  40. * along with this program. If not, see <http://www.gnu.org/licenses/>
  41. *
  42. */
  43. namespace OCA\User_LDAP;
  44. use OC\Cache\CappedMemoryCache;
  45. use OCP\Group\Backend\IGetDisplayNameBackend;
  46. use OCP\GroupInterface;
  47. use OCP\ILogger;
  48. class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
  49. protected $enabled = false;
  50. /**
  51. * @var string[] $cachedGroupMembers array of users with gid as key
  52. */
  53. protected $cachedGroupMembers;
  54. /**
  55. * @var string[] $cachedGroupsByMember array of groups with uid as key
  56. */
  57. protected $cachedGroupsByMember;
  58. /**
  59. * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
  60. */
  61. protected $cachedNestedGroups;
  62. /** @var GroupPluginManager */
  63. protected $groupPluginManager;
  64. public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
  65. parent::__construct($access);
  66. $filter = $this->access->connection->ldapGroupFilter;
  67. $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
  68. if (!empty($filter) && !empty($gassoc)) {
  69. $this->enabled = true;
  70. }
  71. $this->cachedGroupMembers = new CappedMemoryCache();
  72. $this->cachedGroupsByMember = new CappedMemoryCache();
  73. $this->cachedNestedGroups = new CappedMemoryCache();
  74. $this->groupPluginManager = $groupPluginManager;
  75. }
  76. /**
  77. * is user in group?
  78. * @param string $uid uid of the user
  79. * @param string $gid gid of the group
  80. * @return bool
  81. *
  82. * Checks whether the user is member of a group or not.
  83. */
  84. public function inGroup($uid, $gid) {
  85. if (!$this->enabled) {
  86. return false;
  87. }
  88. $cacheKey = 'inGroup'.$uid.':'.$gid;
  89. $inGroup = $this->access->connection->getFromCache($cacheKey);
  90. if (!is_null($inGroup)) {
  91. return (bool)$inGroup;
  92. }
  93. $userDN = $this->access->username2dn($uid);
  94. if (isset($this->cachedGroupMembers[$gid])) {
  95. $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
  96. return $isInGroup;
  97. }
  98. $cacheKeyMembers = 'inGroup-members:'.$gid;
  99. $members = $this->access->connection->getFromCache($cacheKeyMembers);
  100. if (!is_null($members)) {
  101. $this->cachedGroupMembers[$gid] = $members;
  102. $isInGroup = in_array($userDN, $members, true);
  103. $this->access->connection->writeToCache($cacheKey, $isInGroup);
  104. return $isInGroup;
  105. }
  106. $groupDN = $this->access->groupname2dn($gid);
  107. // just in case
  108. if (!$groupDN || !$userDN) {
  109. $this->access->connection->writeToCache($cacheKey, false);
  110. return false;
  111. }
  112. //check primary group first
  113. if ($gid === $this->getUserPrimaryGroup($userDN)) {
  114. $this->access->connection->writeToCache($cacheKey, true);
  115. return true;
  116. }
  117. //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
  118. $members = $this->_groupMembers($groupDN);
  119. if (!is_array($members) || count($members) === 0) {
  120. $this->access->connection->writeToCache($cacheKey, false);
  121. return false;
  122. }
  123. //extra work if we don't get back user DNs
  124. if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
  125. $dns = [];
  126. $filterParts = [];
  127. $bytes = 0;
  128. foreach ($members as $mid) {
  129. $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
  130. $filterParts[] = $filter;
  131. $bytes += strlen($filter);
  132. if ($bytes >= 9000000) {
  133. // AD has a default input buffer of 10 MB, we do not want
  134. // to take even the chance to exceed it
  135. $filter = $this->access->combineFilterWithOr($filterParts);
  136. $bytes = 0;
  137. $filterParts = [];
  138. $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
  139. $dns = array_merge($dns, $users);
  140. }
  141. }
  142. if (count($filterParts) > 0) {
  143. $filter = $this->access->combineFilterWithOr($filterParts);
  144. $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
  145. $dns = array_merge($dns, $users);
  146. }
  147. $members = $dns;
  148. }
  149. $isInGroup = in_array($userDN, $members);
  150. $this->access->connection->writeToCache($cacheKey, $isInGroup);
  151. $this->access->connection->writeToCache($cacheKeyMembers, $members);
  152. $this->cachedGroupMembers[$gid] = $members;
  153. return $isInGroup;
  154. }
  155. /**
  156. * @param string $dnGroup
  157. * @return array
  158. *
  159. * For a group that has user membership defined by an LDAP search url attribute returns the users
  160. * that match the search url otherwise returns an empty array.
  161. */
  162. public function getDynamicGroupMembers($dnGroup) {
  163. $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
  164. if (empty($dynamicGroupMemberURL)) {
  165. return [];
  166. }
  167. $dynamicMembers = [];
  168. $memberURLs = $this->access->readAttribute(
  169. $dnGroup,
  170. $dynamicGroupMemberURL,
  171. $this->access->connection->ldapGroupFilter
  172. );
  173. if ($memberURLs !== false) {
  174. // this group has the 'memberURL' attribute so this is a dynamic group
  175. // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
  176. // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
  177. $pos = strpos($memberURLs[0], '(');
  178. if ($pos !== false) {
  179. $memberUrlFilter = substr($memberURLs[0], $pos);
  180. $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
  181. $dynamicMembers = [];
  182. foreach ($foundMembers as $value) {
  183. $dynamicMembers[$value['dn'][0]] = 1;
  184. }
  185. } else {
  186. \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
  187. 'of group ' . $dnGroup, ILogger::DEBUG);
  188. }
  189. }
  190. return $dynamicMembers;
  191. }
  192. /**
  193. * @param string $dnGroup
  194. * @param array|null &$seen
  195. * @return array|mixed|null
  196. * @throws \OC\ServerNotAvailableException
  197. */
  198. private function _groupMembers($dnGroup, &$seen = null) {
  199. if ($seen === null) {
  200. $seen = [];
  201. }
  202. $allMembers = [];
  203. if (array_key_exists($dnGroup, $seen)) {
  204. // avoid loops
  205. return [];
  206. }
  207. // used extensively in cron job, caching makes sense for nested groups
  208. $cacheKey = '_groupMembers'.$dnGroup;
  209. $groupMembers = $this->access->connection->getFromCache($cacheKey);
  210. if ($groupMembers !== null) {
  211. return $groupMembers;
  212. }
  213. $seen[$dnGroup] = 1;
  214. $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
  215. if (is_array($members)) {
  216. $fetcher = function ($memberDN, &$seen) {
  217. return $this->_groupMembers($memberDN, $seen);
  218. };
  219. $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
  220. }
  221. $allMembers += $this->getDynamicGroupMembers($dnGroup);
  222. $this->access->connection->writeToCache($cacheKey, $allMembers);
  223. return $allMembers;
  224. }
  225. /**
  226. * @param string $DN
  227. * @param array|null &$seen
  228. * @return array
  229. * @throws \OC\ServerNotAvailableException
  230. */
  231. private function _getGroupDNsFromMemberOf($DN) {
  232. $groups = $this->access->readAttribute($DN, 'memberOf');
  233. if (!is_array($groups)) {
  234. return [];
  235. }
  236. $fetcher = function ($groupDN) {
  237. if (isset($this->cachedNestedGroups[$groupDN])) {
  238. $nestedGroups = $this->cachedNestedGroups[$groupDN];
  239. } else {
  240. $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
  241. if (!is_array($nestedGroups)) {
  242. $nestedGroups = [];
  243. }
  244. $this->cachedNestedGroups[$groupDN] = $nestedGroups;
  245. }
  246. return $nestedGroups;
  247. };
  248. $groups = $this->walkNestedGroups($DN, $fetcher, $groups);
  249. return $this->filterValidGroups($groups);
  250. }
  251. /**
  252. * @param string $dn
  253. * @param \Closure $fetcher args: string $dn, array $seen, returns: string[] of dns
  254. * @param array $list
  255. * @return array
  256. */
  257. private function walkNestedGroups(string $dn, \Closure $fetcher, array $list): array {
  258. $nesting = (int) $this->access->connection->ldapNestedGroups;
  259. // depending on the input, we either have a list of DNs or a list of LDAP records
  260. // also, the output expects either DNs or records. Testing the first element should suffice.
  261. $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
  262. if ($nesting !== 1) {
  263. if ($recordMode) {
  264. // the keys are numeric, but should hold the DN
  265. return array_reduce($list, function ($transformed, $record) use ($dn) {
  266. if ($record['dn'][0] != $dn) {
  267. $transformed[$record['dn'][0]] = $record;
  268. }
  269. return $transformed;
  270. }, []);
  271. }
  272. return $list;
  273. }
  274. $seen = [];
  275. while ($record = array_pop($list)) {
  276. $recordDN = $recordMode ? $record['dn'][0] : $record;
  277. if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
  278. // Prevent loops
  279. continue;
  280. }
  281. $fetched = $fetcher($record, $seen);
  282. $list = array_merge($list, $fetched);
  283. $seen[$recordDN] = $record;
  284. }
  285. return $recordMode ? $seen : array_keys($seen);
  286. }
  287. /**
  288. * translates a gidNumber into an ownCloud internal name
  289. * @param string $gid as given by gidNumber on POSIX LDAP
  290. * @param string $dn a DN that belongs to the same domain as the group
  291. * @return string|bool
  292. */
  293. public function gidNumber2Name($gid, $dn) {
  294. $cacheKey = 'gidNumberToName' . $gid;
  295. $groupName = $this->access->connection->getFromCache($cacheKey);
  296. if (!is_null($groupName) && isset($groupName)) {
  297. return $groupName;
  298. }
  299. //we need to get the DN from LDAP
  300. $filter = $this->access->combineFilterWithAnd([
  301. $this->access->connection->ldapGroupFilter,
  302. 'objectClass=posixGroup',
  303. $this->access->connection->ldapGidNumber . '=' . $gid
  304. ]);
  305. $result = $this->access->searchGroups($filter, ['dn'], 1);
  306. if (empty($result)) {
  307. return false;
  308. }
  309. $dn = $result[0]['dn'][0];
  310. //and now the group name
  311. //NOTE once we have separate ownCloud group IDs and group names we can
  312. //directly read the display name attribute instead of the DN
  313. $name = $this->access->dn2groupname($dn);
  314. $this->access->connection->writeToCache($cacheKey, $name);
  315. return $name;
  316. }
  317. /**
  318. * returns the entry's gidNumber
  319. * @param string $dn
  320. * @param string $attribute
  321. * @return string|bool
  322. */
  323. private function getEntryGidNumber($dn, $attribute) {
  324. $value = $this->access->readAttribute($dn, $attribute);
  325. if (is_array($value) && !empty($value)) {
  326. return $value[0];
  327. }
  328. return false;
  329. }
  330. /**
  331. * returns the group's primary ID
  332. * @param string $dn
  333. * @return string|bool
  334. */
  335. public function getGroupGidNumber($dn) {
  336. return $this->getEntryGidNumber($dn, 'gidNumber');
  337. }
  338. /**
  339. * returns the user's gidNumber
  340. * @param string $dn
  341. * @return string|bool
  342. */
  343. public function getUserGidNumber($dn) {
  344. $gidNumber = false;
  345. if ($this->access->connection->hasGidNumber) {
  346. $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
  347. if ($gidNumber === false) {
  348. $this->access->connection->hasGidNumber = false;
  349. }
  350. }
  351. return $gidNumber;
  352. }
  353. /**
  354. * returns a filter for a "users has specific gid" search or count operation
  355. *
  356. * @param string $groupDN
  357. * @param string $search
  358. * @return string
  359. * @throws \Exception
  360. */
  361. private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
  362. $groupID = $this->getGroupGidNumber($groupDN);
  363. if ($groupID === false) {
  364. throw new \Exception('Not a valid group');
  365. }
  366. $filterParts = [];
  367. $filterParts[] = $this->access->getFilterForUserCount();
  368. if ($search !== '') {
  369. $filterParts[] = $this->access->getFilterPartForUserSearch($search);
  370. }
  371. $filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
  372. return $this->access->combineFilterWithAnd($filterParts);
  373. }
  374. /**
  375. * returns a list of users that have the given group as gid number
  376. *
  377. * @param string $groupDN
  378. * @param string $search
  379. * @param int $limit
  380. * @param int $offset
  381. * @return string[]
  382. */
  383. public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
  384. try {
  385. $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
  386. $users = $this->access->fetchListOfUsers(
  387. $filter,
  388. [$this->access->connection->ldapUserDisplayName, 'dn'],
  389. $limit,
  390. $offset
  391. );
  392. return $this->access->nextcloudUserNames($users);
  393. } catch (\Exception $e) {
  394. return [];
  395. }
  396. }
  397. /**
  398. * returns the number of users that have the given group as gid number
  399. *
  400. * @param string $groupDN
  401. * @param string $search
  402. * @param int $limit
  403. * @param int $offset
  404. * @return int
  405. */
  406. public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
  407. try {
  408. $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
  409. $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
  410. return (int)$users;
  411. } catch (\Exception $e) {
  412. return 0;
  413. }
  414. }
  415. /**
  416. * gets the gidNumber of a user
  417. * @param string $dn
  418. * @return string
  419. */
  420. public function getUserGroupByGid($dn) {
  421. $groupID = $this->getUserGidNumber($dn);
  422. if ($groupID !== false) {
  423. $groupName = $this->gidNumber2Name($groupID, $dn);
  424. if ($groupName !== false) {
  425. return $groupName;
  426. }
  427. }
  428. return false;
  429. }
  430. /**
  431. * translates a primary group ID into an Nextcloud internal name
  432. * @param string $gid as given by primaryGroupID on AD
  433. * @param string $dn a DN that belongs to the same domain as the group
  434. * @return string|bool
  435. */
  436. public function primaryGroupID2Name($gid, $dn) {
  437. $cacheKey = 'primaryGroupIDtoName';
  438. $groupNames = $this->access->connection->getFromCache($cacheKey);
  439. if (!is_null($groupNames) && isset($groupNames[$gid])) {
  440. return $groupNames[$gid];
  441. }
  442. $domainObjectSid = $this->access->getSID($dn);
  443. if ($domainObjectSid === false) {
  444. return false;
  445. }
  446. //we need to get the DN from LDAP
  447. $filter = $this->access->combineFilterWithAnd([
  448. $this->access->connection->ldapGroupFilter,
  449. 'objectsid=' . $domainObjectSid . '-' . $gid
  450. ]);
  451. $result = $this->access->searchGroups($filter, ['dn'], 1);
  452. if (empty($result)) {
  453. return false;
  454. }
  455. $dn = $result[0]['dn'][0];
  456. //and now the group name
  457. //NOTE once we have separate Nextcloud group IDs and group names we can
  458. //directly read the display name attribute instead of the DN
  459. $name = $this->access->dn2groupname($dn);
  460. $this->access->connection->writeToCache($cacheKey, $name);
  461. return $name;
  462. }
  463. /**
  464. * returns the entry's primary group ID
  465. * @param string $dn
  466. * @param string $attribute
  467. * @return string|bool
  468. */
  469. private function getEntryGroupID($dn, $attribute) {
  470. $value = $this->access->readAttribute($dn, $attribute);
  471. if (is_array($value) && !empty($value)) {
  472. return $value[0];
  473. }
  474. return false;
  475. }
  476. /**
  477. * returns the group's primary ID
  478. * @param string $dn
  479. * @return string|bool
  480. */
  481. public function getGroupPrimaryGroupID($dn) {
  482. return $this->getEntryGroupID($dn, 'primaryGroupToken');
  483. }
  484. /**
  485. * returns the user's primary group ID
  486. * @param string $dn
  487. * @return string|bool
  488. */
  489. public function getUserPrimaryGroupIDs($dn) {
  490. $primaryGroupID = false;
  491. if ($this->access->connection->hasPrimaryGroups) {
  492. $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
  493. if ($primaryGroupID === false) {
  494. $this->access->connection->hasPrimaryGroups = false;
  495. }
  496. }
  497. return $primaryGroupID;
  498. }
  499. /**
  500. * returns a filter for a "users in primary group" search or count operation
  501. *
  502. * @param string $groupDN
  503. * @param string $search
  504. * @return string
  505. * @throws \Exception
  506. */
  507. private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
  508. $groupID = $this->getGroupPrimaryGroupID($groupDN);
  509. if ($groupID === false) {
  510. throw new \Exception('Not a valid group');
  511. }
  512. $filterParts = [];
  513. $filterParts[] = $this->access->getFilterForUserCount();
  514. if ($search !== '') {
  515. $filterParts[] = $this->access->getFilterPartForUserSearch($search);
  516. }
  517. $filterParts[] = 'primaryGroupID=' . $groupID;
  518. return $this->access->combineFilterWithAnd($filterParts);
  519. }
  520. /**
  521. * returns a list of users that have the given group as primary group
  522. *
  523. * @param string $groupDN
  524. * @param string $search
  525. * @param int $limit
  526. * @param int $offset
  527. * @return string[]
  528. */
  529. public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
  530. try {
  531. $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
  532. $users = $this->access->fetchListOfUsers(
  533. $filter,
  534. [$this->access->connection->ldapUserDisplayName, 'dn'],
  535. $limit,
  536. $offset
  537. );
  538. return $this->access->nextcloudUserNames($users);
  539. } catch (\Exception $e) {
  540. return [];
  541. }
  542. }
  543. /**
  544. * returns the number of users that have the given group as primary group
  545. *
  546. * @param string $groupDN
  547. * @param string $search
  548. * @param int $limit
  549. * @param int $offset
  550. * @return int
  551. */
  552. public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
  553. try {
  554. $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
  555. $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
  556. return (int)$users;
  557. } catch (\Exception $e) {
  558. return 0;
  559. }
  560. }
  561. /**
  562. * gets the primary group of a user
  563. * @param string $dn
  564. * @return string
  565. */
  566. public function getUserPrimaryGroup($dn) {
  567. $groupID = $this->getUserPrimaryGroupIDs($dn);
  568. if ($groupID !== false) {
  569. $groupName = $this->primaryGroupID2Name($groupID, $dn);
  570. if ($groupName !== false) {
  571. return $groupName;
  572. }
  573. }
  574. return false;
  575. }
  576. /**
  577. * Get all groups a user belongs to
  578. * @param string $uid Name of the user
  579. * @return array with group names
  580. *
  581. * This function fetches all groups a user belongs to. It does not check
  582. * if the user exists at all.
  583. *
  584. * This function includes groups based on dynamic group membership.
  585. */
  586. public function getUserGroups($uid) {
  587. if (!$this->enabled) {
  588. return [];
  589. }
  590. $cacheKey = 'getUserGroups'.$uid;
  591. $userGroups = $this->access->connection->getFromCache($cacheKey);
  592. if (!is_null($userGroups)) {
  593. return $userGroups;
  594. }
  595. $userDN = $this->access->username2dn($uid);
  596. if (!$userDN) {
  597. $this->access->connection->writeToCache($cacheKey, []);
  598. return [];
  599. }
  600. $groups = [];
  601. $primaryGroup = $this->getUserPrimaryGroup($userDN);
  602. $gidGroupName = $this->getUserGroupByGid($userDN);
  603. $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
  604. if (!empty($dynamicGroupMemberURL)) {
  605. // look through dynamic groups to add them to the result array if needed
  606. $groupsToMatch = $this->access->fetchListOfGroups(
  607. $this->access->connection->ldapGroupFilter,['dn',$dynamicGroupMemberURL]);
  608. foreach ($groupsToMatch as $dynamicGroup) {
  609. if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
  610. continue;
  611. }
  612. $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
  613. if ($pos !== false) {
  614. $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
  615. // apply filter via ldap search to see if this user is in this
  616. // dynamic group
  617. $userMatch = $this->access->readAttribute(
  618. $userDN,
  619. $this->access->connection->ldapUserDisplayName,
  620. $memberUrlFilter
  621. );
  622. if ($userMatch !== false) {
  623. // match found so this user is in this group
  624. $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
  625. if (is_string($groupName)) {
  626. // be sure to never return false if the dn could not be
  627. // resolved to a name, for whatever reason.
  628. $groups[] = $groupName;
  629. }
  630. }
  631. } else {
  632. \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
  633. 'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
  634. }
  635. }
  636. }
  637. // if possible, read out membership via memberOf. It's far faster than
  638. // performing a search, which still is a fallback later.
  639. // memberof doesn't support memberuid, so skip it here.
  640. if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
  641. && (int)$this->access->connection->useMemberOfToDetectMembership === 1
  642. && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
  643. ) {
  644. $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
  645. if (is_array($groupDNs)) {
  646. foreach ($groupDNs as $dn) {
  647. $groupName = $this->access->dn2groupname($dn);
  648. if (is_string($groupName)) {
  649. // be sure to never return false if the dn could not be
  650. // resolved to a name, for whatever reason.
  651. $groups[] = $groupName;
  652. }
  653. }
  654. }
  655. if ($primaryGroup !== false) {
  656. $groups[] = $primaryGroup;
  657. }
  658. if ($gidGroupName !== false) {
  659. $groups[] = $gidGroupName;
  660. }
  661. $this->access->connection->writeToCache($cacheKey, $groups);
  662. return $groups;
  663. }
  664. //uniqueMember takes DN, memberuid the uid, so we need to distinguish
  665. if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
  666. || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
  667. ) {
  668. $uid = $userDN;
  669. } elseif (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
  670. $result = $this->access->readAttribute($userDN, 'uid');
  671. if ($result === false) {
  672. \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
  673. $this->access->connection->ldapHost, ILogger::DEBUG);
  674. $uid = false;
  675. } else {
  676. $uid = $result[0];
  677. }
  678. } else {
  679. // just in case
  680. $uid = $userDN;
  681. }
  682. if ($uid !== false) {
  683. if (isset($this->cachedGroupsByMember[$uid])) {
  684. $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
  685. } else {
  686. $groupsByMember = array_values($this->getGroupsByMember($uid));
  687. $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
  688. $this->cachedGroupsByMember[$uid] = $groupsByMember;
  689. $groups = array_merge($groups, $groupsByMember);
  690. }
  691. }
  692. if ($primaryGroup !== false) {
  693. $groups[] = $primaryGroup;
  694. }
  695. if ($gidGroupName !== false) {
  696. $groups[] = $gidGroupName;
  697. }
  698. $groups = array_unique($groups, SORT_LOCALE_STRING);
  699. $this->access->connection->writeToCache($cacheKey, $groups);
  700. return $groups;
  701. }
  702. /**
  703. * @param string $dn
  704. * @param array|null &$seen
  705. * @return array
  706. */
  707. private function getGroupsByMember($dn, &$seen = null) {
  708. if ($seen === null) {
  709. $seen = [];
  710. }
  711. if (array_key_exists($dn, $seen)) {
  712. // avoid loops
  713. return [];
  714. }
  715. $allGroups = [];
  716. $seen[$dn] = true;
  717. $filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
  718. $groups = $this->access->fetchListOfGroups($filter,
  719. [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
  720. if (is_array($groups)) {
  721. $fetcher = function ($dn, &$seen) {
  722. if (is_array($dn) && isset($dn['dn'][0])) {
  723. $dn = $dn['dn'][0];
  724. }
  725. return $this->getGroupsByMember($dn, $seen);
  726. };
  727. $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
  728. }
  729. $visibleGroups = $this->filterValidGroups($allGroups);
  730. return array_intersect_key($allGroups, $visibleGroups);
  731. }
  732. /**
  733. * get a list of all users in a group
  734. *
  735. * @param string $gid
  736. * @param string $search
  737. * @param int $limit
  738. * @param int $offset
  739. * @return array with user ids
  740. * @throws \Exception
  741. */
  742. public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
  743. if (!$this->enabled) {
  744. return [];
  745. }
  746. if (!$this->groupExists($gid)) {
  747. return [];
  748. }
  749. $search = $this->access->escapeFilterPart($search, true);
  750. $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
  751. // check for cache of the exact query
  752. $groupUsers = $this->access->connection->getFromCache($cacheKey);
  753. if (!is_null($groupUsers)) {
  754. return $groupUsers;
  755. }
  756. // check for cache of the query without limit and offset
  757. $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
  758. if (!is_null($groupUsers)) {
  759. $groupUsers = array_slice($groupUsers, $offset, $limit);
  760. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  761. return $groupUsers;
  762. }
  763. if ($limit === -1) {
  764. $limit = null;
  765. }
  766. $groupDN = $this->access->groupname2dn($gid);
  767. if (!$groupDN) {
  768. // group couldn't be found, return empty resultset
  769. $this->access->connection->writeToCache($cacheKey, []);
  770. return [];
  771. }
  772. $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
  773. $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
  774. $members = $this->_groupMembers($groupDN);
  775. if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
  776. //in case users could not be retrieved, return empty result set
  777. $this->access->connection->writeToCache($cacheKey, []);
  778. return [];
  779. }
  780. $groupUsers = [];
  781. $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
  782. $attrs = $this->access->userManager->getAttributes(true);
  783. foreach ($members as $member) {
  784. if ($isMemberUid) {
  785. //we got uids, need to get their DNs to 'translate' them to user names
  786. $filter = $this->access->combineFilterWithAnd([
  787. str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
  788. $this->access->combineFilterWithAnd([
  789. $this->access->getFilterPartForUserSearch($search),
  790. $this->access->connection->ldapUserFilter
  791. ])
  792. ]);
  793. $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
  794. if (count($ldap_users) < 1) {
  795. continue;
  796. }
  797. $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
  798. } else {
  799. //we got DNs, check if we need to filter by search or we can give back all of them
  800. $uid = $this->access->dn2username($member);
  801. if (!$uid) {
  802. continue;
  803. }
  804. $cacheKey = 'userExistsOnLDAP' . $uid;
  805. $userExists = $this->access->connection->getFromCache($cacheKey);
  806. if ($userExists === false) {
  807. continue;
  808. }
  809. if ($userExists === null || $search !== '') {
  810. if (!$this->access->readAttribute($member,
  811. $this->access->connection->ldapUserDisplayName,
  812. $this->access->combineFilterWithAnd([
  813. $this->access->getFilterPartForUserSearch($search),
  814. $this->access->connection->ldapUserFilter
  815. ]))) {
  816. if ($search === '') {
  817. $this->access->connection->writeToCache($cacheKey, false);
  818. }
  819. continue;
  820. }
  821. $this->access->connection->writeToCache($cacheKey, true);
  822. }
  823. $groupUsers[] = $uid;
  824. }
  825. }
  826. $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
  827. natsort($groupUsers);
  828. $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
  829. $groupUsers = array_slice($groupUsers, $offset, $limit);
  830. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  831. return $groupUsers;
  832. }
  833. /**
  834. * returns the number of users in a group, who match the search term
  835. * @param string $gid the internal group name
  836. * @param string $search optional, a search string
  837. * @return int|bool
  838. */
  839. public function countUsersInGroup($gid, $search = '') {
  840. if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
  841. return $this->groupPluginManager->countUsersInGroup($gid, $search);
  842. }
  843. $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
  844. if (!$this->enabled || !$this->groupExists($gid)) {
  845. return false;
  846. }
  847. $groupUsers = $this->access->connection->getFromCache($cacheKey);
  848. if (!is_null($groupUsers)) {
  849. return $groupUsers;
  850. }
  851. $groupDN = $this->access->groupname2dn($gid);
  852. if (!$groupDN) {
  853. // group couldn't be found, return empty result set
  854. $this->access->connection->writeToCache($cacheKey, false);
  855. return false;
  856. }
  857. $members = $this->_groupMembers($groupDN);
  858. $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
  859. if (!$members && $primaryUserCount === 0) {
  860. //in case users could not be retrieved, return empty result set
  861. $this->access->connection->writeToCache($cacheKey, false);
  862. return false;
  863. }
  864. if ($search === '') {
  865. $groupUsers = count($members) + $primaryUserCount;
  866. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  867. return $groupUsers;
  868. }
  869. $search = $this->access->escapeFilterPart($search, true);
  870. $isMemberUid =
  871. (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
  872. === 'memberuid');
  873. //we need to apply the search filter
  874. //alternatives that need to be checked:
  875. //a) get all users by search filter and array_intersect them
  876. //b) a, but only when less than 1k 10k ?k users like it is
  877. //c) put all DNs|uids in a LDAP filter, combine with the search string
  878. // and let it count.
  879. //For now this is not important, because the only use of this method
  880. //does not supply a search string
  881. $groupUsers = [];
  882. foreach ($members as $member) {
  883. if ($isMemberUid) {
  884. //we got uids, need to get their DNs to 'translate' them to user names
  885. $filter = $this->access->combineFilterWithAnd([
  886. str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
  887. $this->access->getFilterPartForUserSearch($search)
  888. ]);
  889. $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
  890. if (count($ldap_users) < 1) {
  891. continue;
  892. }
  893. $groupUsers[] = $this->access->dn2username($ldap_users[0]);
  894. } else {
  895. //we need to apply the search filter now
  896. if (!$this->access->readAttribute($member,
  897. $this->access->connection->ldapUserDisplayName,
  898. $this->access->getFilterPartForUserSearch($search))) {
  899. continue;
  900. }
  901. // dn2username will also check if the users belong to the allowed base
  902. if ($ocname = $this->access->dn2username($member)) {
  903. $groupUsers[] = $ocname;
  904. }
  905. }
  906. }
  907. //and get users that have the group as primary
  908. $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
  909. return count($groupUsers) + $primaryUsers;
  910. }
  911. /**
  912. * get a list of all groups
  913. *
  914. * @param string $search
  915. * @param $limit
  916. * @param int $offset
  917. * @return array with group names
  918. *
  919. * Returns a list with all groups (used by getGroups)
  920. */
  921. protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
  922. if (!$this->enabled) {
  923. return [];
  924. }
  925. $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
  926. //Check cache before driving unnecessary searches
  927. \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
  928. $ldap_groups = $this->access->connection->getFromCache($cacheKey);
  929. if (!is_null($ldap_groups)) {
  930. return $ldap_groups;
  931. }
  932. // if we'd pass -1 to LDAP search, we'd end up in a Protocol
  933. // error. With a limit of 0, we get 0 results. So we pass null.
  934. if ($limit <= 0) {
  935. $limit = null;
  936. }
  937. $filter = $this->access->combineFilterWithAnd([
  938. $this->access->connection->ldapGroupFilter,
  939. $this->access->getFilterPartForGroupSearch($search)
  940. ]);
  941. \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
  942. $ldap_groups = $this->access->fetchListOfGroups($filter,
  943. [$this->access->connection->ldapGroupDisplayName, 'dn'],
  944. $limit,
  945. $offset);
  946. $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
  947. $this->access->connection->writeToCache($cacheKey, $ldap_groups);
  948. return $ldap_groups;
  949. }
  950. /**
  951. * get a list of all groups using a paged search
  952. *
  953. * @param string $search
  954. * @param int $limit
  955. * @param int $offset
  956. * @return array with group names
  957. *
  958. * Returns a list with all groups
  959. * Uses a paged search if available to override a
  960. * server side search limit.
  961. * (active directory has a limit of 1000 by default)
  962. */
  963. public function getGroups($search = '', $limit = -1, $offset = 0) {
  964. if (!$this->enabled) {
  965. return [];
  966. }
  967. $search = $this->access->escapeFilterPart($search, true);
  968. $pagingSize = (int)$this->access->connection->ldapPagingSize;
  969. if ($pagingSize <= 0) {
  970. return $this->getGroupsChunk($search, $limit, $offset);
  971. }
  972. $maxGroups = 100000; // limit max results (just for safety reasons)
  973. if ($limit > -1) {
  974. $overallLimit = min($limit + $offset, $maxGroups);
  975. } else {
  976. $overallLimit = $maxGroups;
  977. }
  978. $chunkOffset = $offset;
  979. $allGroups = [];
  980. while ($chunkOffset < $overallLimit) {
  981. $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
  982. $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
  983. $nread = count($ldapGroups);
  984. \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
  985. if ($nread) {
  986. $allGroups = array_merge($allGroups, $ldapGroups);
  987. $chunkOffset += $nread;
  988. }
  989. if ($nread < $chunkLimit) {
  990. break;
  991. }
  992. }
  993. return $allGroups;
  994. }
  995. /**
  996. * @param string $group
  997. * @return bool
  998. */
  999. public function groupMatchesFilter($group) {
  1000. return (strripos($group, $this->groupSearch) !== false);
  1001. }
  1002. /**
  1003. * check if a group exists
  1004. * @param string $gid
  1005. * @return bool
  1006. */
  1007. public function groupExists($gid) {
  1008. $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
  1009. if (!is_null($groupExists)) {
  1010. return (bool)$groupExists;
  1011. }
  1012. //getting dn, if false the group does not exist. If dn, it may be mapped
  1013. //only, requires more checking.
  1014. $dn = $this->access->groupname2dn($gid);
  1015. if (!$dn) {
  1016. $this->access->connection->writeToCache('groupExists'.$gid, false);
  1017. return false;
  1018. }
  1019. if(!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
  1020. $this->access->connection->writeToCache('groupExists'.$gid, false);
  1021. return false;
  1022. }
  1023. //if group really still exists, we will be able to read its objectclass
  1024. if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
  1025. $this->access->connection->writeToCache('groupExists'.$gid, false);
  1026. return false;
  1027. }
  1028. $this->access->connection->writeToCache('groupExists'.$gid, true);
  1029. return true;
  1030. }
  1031. protected function filterValidGroups (array $listOfGroups): array {
  1032. $validGroupDNs = [];
  1033. foreach($listOfGroups as $key => $item) {
  1034. $dn = is_string($item) ? $item : $item['dn'][0];
  1035. $gid = $this->access->dn2groupname($dn);
  1036. if(!$gid) {
  1037. continue;
  1038. }
  1039. if($this->groupExists($gid)) {
  1040. $validGroupDNs[$key] = $item;
  1041. }
  1042. }
  1043. return $validGroupDNs;
  1044. }
  1045. /**
  1046. * Check if backend implements actions
  1047. * @param int $actions bitwise-or'ed actions
  1048. * @return boolean
  1049. *
  1050. * Returns the supported actions as int to be
  1051. * compared with GroupInterface::CREATE_GROUP etc.
  1052. */
  1053. public function implementsActions($actions) {
  1054. return (bool)((GroupInterface::COUNT_USERS |
  1055. $this->groupPluginManager->getImplementedActions()) & $actions);
  1056. }
  1057. /**
  1058. * Return access for LDAP interaction.
  1059. * @return Access instance of Access for LDAP interaction
  1060. */
  1061. public function getLDAPAccess($gid) {
  1062. return $this->access;
  1063. }
  1064. /**
  1065. * create a group
  1066. * @param string $gid
  1067. * @return bool
  1068. * @throws \Exception
  1069. */
  1070. public function createGroup($gid) {
  1071. if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
  1072. if ($dn = $this->groupPluginManager->createGroup($gid)) {
  1073. //updates group mapping
  1074. $uuid = $this->access->getUUID($dn, false);
  1075. if (is_string($uuid)) {
  1076. $this->access->mapAndAnnounceIfApplicable(
  1077. $this->access->getGroupMapper(),
  1078. $dn,
  1079. $gid,
  1080. $uuid,
  1081. false
  1082. );
  1083. $this->access->cacheGroupExists($gid);
  1084. }
  1085. }
  1086. return $dn != null;
  1087. }
  1088. throw new \Exception('Could not create group in LDAP backend.');
  1089. }
  1090. /**
  1091. * delete a group
  1092. * @param string $gid gid of the group to delete
  1093. * @return bool
  1094. * @throws \Exception
  1095. */
  1096. public function deleteGroup($gid) {
  1097. if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
  1098. if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
  1099. #delete group in nextcloud internal db
  1100. $this->access->getGroupMapper()->unmap($gid);
  1101. $this->access->connection->writeToCache("groupExists".$gid, false);
  1102. }
  1103. return $ret;
  1104. }
  1105. throw new \Exception('Could not delete group in LDAP backend.');
  1106. }
  1107. /**
  1108. * Add a user to a group
  1109. * @param string $uid Name of the user to add to group
  1110. * @param string $gid Name of the group in which add the user
  1111. * @return bool
  1112. * @throws \Exception
  1113. */
  1114. public function addToGroup($uid, $gid) {
  1115. if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
  1116. if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
  1117. $this->access->connection->clearCache();
  1118. unset($this->cachedGroupMembers[$gid]);
  1119. }
  1120. return $ret;
  1121. }
  1122. throw new \Exception('Could not add user to group in LDAP backend.');
  1123. }
  1124. /**
  1125. * Removes a user from a group
  1126. * @param string $uid Name of the user to remove from group
  1127. * @param string $gid Name of the group from which remove the user
  1128. * @return bool
  1129. * @throws \Exception
  1130. */
  1131. public function removeFromGroup($uid, $gid) {
  1132. if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
  1133. if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
  1134. $this->access->connection->clearCache();
  1135. unset($this->cachedGroupMembers[$gid]);
  1136. }
  1137. return $ret;
  1138. }
  1139. throw new \Exception('Could not remove user from group in LDAP backend.');
  1140. }
  1141. /**
  1142. * Gets group details
  1143. * @param string $gid Name of the group
  1144. * @return array | false
  1145. * @throws \Exception
  1146. */
  1147. public function getGroupDetails($gid) {
  1148. if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
  1149. return $this->groupPluginManager->getGroupDetails($gid);
  1150. }
  1151. throw new \Exception('Could not get group details in LDAP backend.');
  1152. }
  1153. /**
  1154. * Return LDAP connection resource from a cloned connection.
  1155. * The cloned connection needs to be closed manually.
  1156. * of the current access.
  1157. * @param string $gid
  1158. * @return resource of the LDAP connection
  1159. */
  1160. public function getNewLDAPConnection($gid) {
  1161. $connection = clone $this->access->getConnection();
  1162. return $connection->getConnectionResource();
  1163. }
  1164. /**
  1165. * @throws \OC\ServerNotAvailableException
  1166. */
  1167. public function getDisplayName(string $gid): string {
  1168. if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
  1169. return $this->groupPluginManager->getDisplayName($gid);
  1170. }
  1171. $cacheKey = 'group_getDisplayName' . $gid;
  1172. if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
  1173. return $displayName;
  1174. }
  1175. $displayName = $this->access->readAttribute(
  1176. $this->access->groupname2dn($gid),
  1177. $this->access->connection->ldapGroupDisplayName);
  1178. if ($displayName && (count($displayName) > 0)) {
  1179. $displayName = $displayName[0];
  1180. $this->access->connection->writeToCache($cacheKey, $displayName);
  1181. return $displayName;
  1182. }
  1183. return '';
  1184. }
  1185. }