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.

Group_LDAP.php 38KB

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