Group_LDAP.php 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447
  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 Christoph Wurst <christoph@winzerhof-wurst.at>
  13. * @author Clement Wong <git@clement.hk>
  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 Tobias Perschon <tobias@perschon.at>
  24. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  25. * @author Vinicius Cubas Brand <vinicius@eita.org.br>
  26. * @author Xuanwo <xuanwo@yunify.com>
  27. * @author Carl Schwan <carl@carlschwan.eu>
  28. * @author Côme Chilliet <come.chilliet@nextcloud.com>
  29. *
  30. * @license AGPL-3.0
  31. *
  32. * This code is free software: you can redistribute it and/or modify
  33. * it under the terms of the GNU Affero General Public License, version 3,
  34. * as published by the Free Software Foundation.
  35. *
  36. * This program is distributed in the hope that it will be useful,
  37. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  38. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  39. * GNU Affero General Public License for more details.
  40. *
  41. * You should have received a copy of the GNU Affero General Public License, version 3,
  42. * along with this program. If not, see <http://www.gnu.org/licenses/>
  43. *
  44. */
  45. namespace OCA\User_LDAP;
  46. use Exception;
  47. use OC\ServerNotAvailableException;
  48. use OCA\User_LDAP\User\OfflineUser;
  49. use OCP\Cache\CappedMemoryCache;
  50. use OCP\GroupInterface;
  51. use OCP\Group\Backend\ABackend;
  52. use OCP\Group\Backend\IDeleteGroupBackend;
  53. use OCP\Group\Backend\IGetDisplayNameBackend;
  54. use OCP\IConfig;
  55. use OCP\IUserManager;
  56. use OCP\Server;
  57. use Psr\Log\LoggerInterface;
  58. use function json_decode;
  59. class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend {
  60. protected bool $enabled = false;
  61. /** @var CappedMemoryCache<string[]> $cachedGroupMembers array of user DN with gid as key */
  62. protected CappedMemoryCache $cachedGroupMembers;
  63. /** @var CappedMemoryCache<array[]> $cachedGroupsByMember array of groups with user DN as key */
  64. protected CappedMemoryCache $cachedGroupsByMember;
  65. /** @var CappedMemoryCache<string[]> $cachedNestedGroups array of groups with gid (DN) as key */
  66. protected CappedMemoryCache $cachedNestedGroups;
  67. protected GroupPluginManager $groupPluginManager;
  68. protected LoggerInterface $logger;
  69. protected Access $access;
  70. /**
  71. * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
  72. */
  73. protected string $ldapGroupMemberAssocAttr;
  74. private IConfig $config;
  75. private IUserManager $ncUserManager;
  76. public function __construct(
  77. Access $access,
  78. GroupPluginManager $groupPluginManager,
  79. IConfig $config,
  80. IUserManager $ncUserManager
  81. ) {
  82. $this->access = $access;
  83. $filter = $this->access->connection->ldapGroupFilter;
  84. $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
  85. if (!empty($filter) && !empty($gAssoc)) {
  86. $this->enabled = true;
  87. }
  88. $this->cachedGroupMembers = new CappedMemoryCache();
  89. $this->cachedGroupsByMember = new CappedMemoryCache();
  90. $this->cachedNestedGroups = new CappedMemoryCache();
  91. $this->groupPluginManager = $groupPluginManager;
  92. $this->logger = Server::get(LoggerInterface::class);
  93. $this->ldapGroupMemberAssocAttr = strtolower((string)$gAssoc);
  94. $this->config = $config;
  95. $this->ncUserManager = $ncUserManager;
  96. }
  97. /**
  98. * Check if user is in group
  99. *
  100. * @param string $uid uid of the user
  101. * @param string $gid gid of the group
  102. * @throws Exception
  103. * @throws ServerNotAvailableException
  104. */
  105. public function inGroup($uid, $gid): bool {
  106. if (!$this->enabled) {
  107. return false;
  108. }
  109. $cacheKey = 'inGroup' . $uid . ':' . $gid;
  110. $inGroup = $this->access->connection->getFromCache($cacheKey);
  111. if (!is_null($inGroup)) {
  112. return (bool)$inGroup;
  113. }
  114. $userDN = $this->access->username2dn($uid);
  115. if (isset($this->cachedGroupMembers[$gid])) {
  116. return in_array($userDN, $this->cachedGroupMembers[$gid]);
  117. }
  118. $cacheKeyMembers = 'inGroup-members:' . $gid;
  119. $members = $this->access->connection->getFromCache($cacheKeyMembers);
  120. if (!is_null($members)) {
  121. $this->cachedGroupMembers[$gid] = $members;
  122. $isInGroup = in_array($userDN, $members, true);
  123. $this->access->connection->writeToCache($cacheKey, $isInGroup);
  124. return $isInGroup;
  125. }
  126. $groupDN = $this->access->groupname2dn($gid);
  127. // just in case
  128. if (!$groupDN || !$userDN) {
  129. $this->access->connection->writeToCache($cacheKey, false);
  130. return false;
  131. }
  132. //check primary group first
  133. if ($gid === $this->getUserPrimaryGroup($userDN)) {
  134. $this->access->connection->writeToCache($cacheKey, true);
  135. return true;
  136. }
  137. //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
  138. $members = $this->_groupMembers($groupDN);
  139. //extra work if we don't get back user DNs
  140. switch ($this->ldapGroupMemberAssocAttr) {
  141. case 'memberuid':
  142. case 'zimbramailforwardingaddress':
  143. $requestAttributes = $this->access->userManager->getAttributes(true);
  144. $users = [];
  145. $filterParts = [];
  146. $bytes = 0;
  147. foreach ($members as $mid) {
  148. if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
  149. $parts = explode('@', $mid); //making sure we get only the uid
  150. $mid = $parts[0];
  151. }
  152. $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
  153. $filterParts[] = $filter;
  154. $bytes += strlen($filter);
  155. if ($bytes >= 9000000) {
  156. // AD has a default input buffer of 10 MB, we do not want
  157. // to take even the chance to exceed it
  158. // so we fetch results with the filterParts we collected so far
  159. $filter = $this->access->combineFilterWithOr($filterParts);
  160. $search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
  161. $bytes = 0;
  162. $filterParts = [];
  163. $users = array_merge($users, $search);
  164. }
  165. }
  166. if (count($filterParts) > 0) {
  167. // if there are filterParts left we need to add their result
  168. $filter = $this->access->combineFilterWithOr($filterParts);
  169. $search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
  170. $users = array_merge($users, $search);
  171. }
  172. // now we cleanup the users array to get only dns
  173. $dns = [];
  174. foreach ($users as $record) {
  175. $dns[$record['dn'][0]] = 1;
  176. }
  177. $members = array_keys($dns);
  178. break;
  179. }
  180. if (count($members) === 0) {
  181. $this->access->connection->writeToCache($cacheKey, false);
  182. return false;
  183. }
  184. $isInGroup = in_array($userDN, $members);
  185. $this->access->connection->writeToCache($cacheKey, $isInGroup);
  186. $this->access->connection->writeToCache($cacheKeyMembers, $members);
  187. $this->cachedGroupMembers[$gid] = $members;
  188. return $isInGroup;
  189. }
  190. /**
  191. * For a group that has user membership defined by an LDAP search url
  192. * attribute returns the users that match the search url otherwise returns
  193. * an empty array.
  194. *
  195. * @throws ServerNotAvailableException
  196. */
  197. public function getDynamicGroupMembers(string $dnGroup): array {
  198. $dynamicGroupMemberURL = strtolower((string)$this->access->connection->ldapDynamicGroupMemberURL);
  199. if (empty($dynamicGroupMemberURL)) {
  200. return [];
  201. }
  202. $dynamicMembers = [];
  203. $memberURLs = $this->access->readAttribute(
  204. $dnGroup,
  205. $dynamicGroupMemberURL,
  206. $this->access->connection->ldapGroupFilter
  207. );
  208. if ($memberURLs !== false) {
  209. // this group has the 'memberURL' attribute so this is a dynamic group
  210. // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
  211. // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
  212. $pos = strpos($memberURLs[0], '(');
  213. if ($pos !== false) {
  214. $memberUrlFilter = substr($memberURLs[0], $pos);
  215. $foundMembers = $this->access->searchUsers($memberUrlFilter, ['dn']);
  216. $dynamicMembers = [];
  217. foreach ($foundMembers as $value) {
  218. $dynamicMembers[$value['dn'][0]] = 1;
  219. }
  220. } else {
  221. $this->logger->debug('No search filter found on member url of group {dn}',
  222. [
  223. 'app' => 'user_ldap',
  224. 'dn' => $dnGroup,
  225. ]
  226. );
  227. }
  228. }
  229. return $dynamicMembers;
  230. }
  231. /**
  232. * Get group members from dn.
  233. * @psalm-param array<string, bool> $seen List of DN that have already been processed.
  234. * @throws ServerNotAvailableException
  235. */
  236. private function _groupMembers(string $dnGroup, array $seen = [], bool &$recursive = false): array {
  237. if (isset($seen[$dnGroup])) {
  238. $recursive = true;
  239. return [];
  240. }
  241. $seen[$dnGroup] = true;
  242. // used extensively in cron job, caching makes sense for nested groups
  243. $cacheKey = '_groupMembers' . $dnGroup;
  244. $groupMembers = $this->access->connection->getFromCache($cacheKey);
  245. if ($groupMembers !== null) {
  246. return $groupMembers;
  247. }
  248. if ($this->access->connection->ldapNestedGroups
  249. && $this->access->connection->useMemberOfToDetectMembership
  250. && $this->access->connection->hasMemberOfFilterSupport
  251. && $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
  252. ) {
  253. $attemptedLdapMatchingRuleInChain = true;
  254. // Use matching rule 1.2.840.113556.1.4.1941 if available (LDAP_MATCHING_RULE_IN_CHAIN)
  255. $filter = $this->access->combineFilterWithAnd([
  256. $this->access->connection->ldapUserFilter,
  257. $this->access->connection->ldapUserDisplayName . '=*',
  258. 'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
  259. ]);
  260. $memberRecords = $this->access->fetchListOfUsers(
  261. $filter,
  262. $this->access->userManager->getAttributes(true)
  263. );
  264. $result = array_reduce($memberRecords, function ($carry, $record) {
  265. $carry[] = $record['dn'][0];
  266. return $carry;
  267. }, []);
  268. if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
  269. $this->access->connection->writeToCache($cacheKey, $result);
  270. return $result;
  271. } elseif (!empty($memberRecords)) {
  272. $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
  273. $this->access->connection->saveConfiguration();
  274. $this->access->connection->writeToCache($cacheKey, $result);
  275. return $result;
  276. }
  277. // when feature availability is unknown, and the result is empty, continue and test with original approach
  278. }
  279. $allMembers = [];
  280. $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
  281. if (is_array($members)) {
  282. if ((int)$this->access->connection->ldapNestedGroups === 1) {
  283. while ($recordDn = array_shift($members)) {
  284. $nestedMembers = $this->_groupMembers($recordDn, $seen, $recursive);
  285. if (!empty($nestedMembers)) {
  286. // Group, queue its members for processing
  287. $members = array_merge($members, $nestedMembers);
  288. } else {
  289. // User (or empty group, or previously seen group), add it to the member list
  290. $allMembers[] = $recordDn;
  291. }
  292. }
  293. } else {
  294. $allMembers = $members;
  295. }
  296. }
  297. $allMembers += $this->getDynamicGroupMembers($dnGroup);
  298. $allMembers = array_unique($allMembers);
  299. // A group cannot be a member of itself
  300. $index = array_search($dnGroup, $allMembers, true);
  301. if ($index !== false) {
  302. unset($allMembers[$index]);
  303. }
  304. if (!$recursive) {
  305. $this->access->connection->writeToCache($cacheKey, $allMembers);
  306. }
  307. if (isset($attemptedLdapMatchingRuleInChain)
  308. && $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
  309. && !empty($allMembers)
  310. ) {
  311. $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
  312. $this->access->connection->saveConfiguration();
  313. }
  314. return $allMembers;
  315. }
  316. /**
  317. * @return string[]
  318. * @throws ServerNotAvailableException
  319. */
  320. private function _getGroupDNsFromMemberOf(string $dn, array &$seen = []): array {
  321. if (isset($seen[$dn])) {
  322. return [];
  323. }
  324. $seen[$dn] = true;
  325. if (isset($this->cachedNestedGroups[$dn])) {
  326. return $this->cachedNestedGroups[$dn];
  327. }
  328. $allGroups = [];
  329. $groups = $this->access->readAttribute($dn, 'memberOf');
  330. if (is_array($groups)) {
  331. if ((int)$this->access->connection->ldapNestedGroups === 1) {
  332. while ($recordDn = array_shift($groups)) {
  333. $nestedParents = $this->_getGroupDNsFromMemberOf($recordDn, $seen);
  334. $groups = array_merge($groups, $nestedParents);
  335. $allGroups[] = $recordDn;
  336. }
  337. } else {
  338. $allGroups = $groups;
  339. }
  340. }
  341. // We do not perform array_unique here at it is done in getUserGroups later
  342. $this->cachedNestedGroups[$dn] = $allGroups;
  343. return $this->filterValidGroups($allGroups);
  344. }
  345. /**
  346. * Translates a gidNumber into the Nextcloud internal name.
  347. *
  348. * @return string|false The nextcloud internal name.
  349. * @throws Exception
  350. * @throws ServerNotAvailableException
  351. */
  352. public function gidNumber2Name(string $gid, string $dn) {
  353. $cacheKey = 'gidNumberToName' . $gid;
  354. $groupName = $this->access->connection->getFromCache($cacheKey);
  355. if (!is_null($groupName) && isset($groupName)) {
  356. return $groupName;
  357. }
  358. //we need to get the DN from LDAP
  359. $filter = $this->access->combineFilterWithAnd([
  360. $this->access->connection->ldapGroupFilter,
  361. 'objectClass=posixGroup',
  362. $this->access->connection->ldapGidNumber . '=' . $gid
  363. ]);
  364. return $this->getNameOfGroup($filter, $cacheKey) ?? false;
  365. }
  366. /**
  367. * @return string|null|false The name of the group
  368. * @throws ServerNotAvailableException
  369. * @throws Exception
  370. */
  371. private function getNameOfGroup(string $filter, string $cacheKey) {
  372. $result = $this->access->searchGroups($filter, ['dn'], 1);
  373. if (empty($result)) {
  374. $this->access->connection->writeToCache($cacheKey, false);
  375. return null;
  376. }
  377. $dn = $result[0]['dn'][0];
  378. //and now the group name
  379. //NOTE once we have separate Nextcloud group IDs and group names we can
  380. //directly read the display name attribute instead of the DN
  381. $name = $this->access->dn2groupname($dn);
  382. $this->access->connection->writeToCache($cacheKey, $name);
  383. return $name;
  384. }
  385. /**
  386. * @return string|bool The entry's gidNumber
  387. * @throws ServerNotAvailableException
  388. */
  389. private function getEntryGidNumber(string $dn, string $attribute) {
  390. $value = $this->access->readAttribute($dn, $attribute);
  391. if (is_array($value) && !empty($value)) {
  392. return $value[0];
  393. }
  394. return false;
  395. }
  396. /**
  397. * @return string|bool The group's gidNumber
  398. * @throws ServerNotAvailableException
  399. */
  400. public function getGroupGidNumber(string $dn) {
  401. return $this->getEntryGidNumber($dn, 'gidNumber');
  402. }
  403. /**
  404. * @return string|bool The user's gidNumber
  405. * @throws ServerNotAvailableException
  406. */
  407. public function getUserGidNumber(string $dn) {
  408. $gidNumber = false;
  409. if ($this->access->connection->hasGidNumber) {
  410. // FIXME: when $dn does not exist on LDAP anymore, this will be set wrongly to false :/
  411. $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
  412. if ($gidNumber === false) {
  413. $this->access->connection->hasGidNumber = false;
  414. }
  415. }
  416. return $gidNumber;
  417. }
  418. /**
  419. * @throws ServerNotAvailableException
  420. * @throws Exception
  421. */
  422. private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
  423. $groupID = $this->getGroupGidNumber($groupDN);
  424. if ($groupID === false) {
  425. throw new Exception('Not a valid group');
  426. }
  427. $filterParts = [];
  428. $filterParts[] = $this->access->getFilterForUserCount();
  429. if ($search !== '') {
  430. $filterParts[] = $this->access->getFilterPartForUserSearch($search);
  431. }
  432. $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
  433. return $this->access->combineFilterWithAnd($filterParts);
  434. }
  435. /**
  436. * @return array<int,string> A list of users that have the given group as gid number
  437. * @throws ServerNotAvailableException
  438. */
  439. public function getUsersInGidNumber(
  440. string $groupDN,
  441. string $search = '',
  442. ?int $limit = -1,
  443. ?int $offset = 0
  444. ): array {
  445. try {
  446. $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
  447. $users = $this->access->fetchListOfUsers(
  448. $filter,
  449. [$this->access->connection->ldapUserDisplayName, 'dn'],
  450. $limit,
  451. $offset
  452. );
  453. return $this->access->nextcloudUserNames($users);
  454. } catch (ServerNotAvailableException $e) {
  455. throw $e;
  456. } catch (Exception $e) {
  457. return [];
  458. }
  459. }
  460. /**
  461. * @throws ServerNotAvailableException
  462. * @return false|string
  463. */
  464. public function getUserGroupByGid(string $dn) {
  465. $groupID = $this->getUserGidNumber($dn);
  466. if ($groupID !== false) {
  467. $groupName = $this->gidNumber2Name($groupID, $dn);
  468. if ($groupName !== false) {
  469. return $groupName;
  470. }
  471. }
  472. return false;
  473. }
  474. /**
  475. * Translates a primary group ID into an Nextcloud internal name
  476. *
  477. * @return string|false
  478. * @throws Exception
  479. * @throws ServerNotAvailableException
  480. */
  481. public function primaryGroupID2Name(string $gid, string $dn) {
  482. $cacheKey = 'primaryGroupIDtoName_' . $gid;
  483. $groupName = $this->access->connection->getFromCache($cacheKey);
  484. if (!is_null($groupName)) {
  485. return $groupName;
  486. }
  487. $domainObjectSid = $this->access->getSID($dn);
  488. if ($domainObjectSid === false) {
  489. return false;
  490. }
  491. //we need to get the DN from LDAP
  492. $filter = $this->access->combineFilterWithAnd([
  493. $this->access->connection->ldapGroupFilter,
  494. 'objectsid=' . $domainObjectSid . '-' . $gid
  495. ]);
  496. return $this->getNameOfGroup($filter, $cacheKey) ?? false;
  497. }
  498. /**
  499. * @return string|false The entry's group Id
  500. * @throws ServerNotAvailableException
  501. */
  502. private function getEntryGroupID(string $dn, string $attribute) {
  503. $value = $this->access->readAttribute($dn, $attribute);
  504. if (is_array($value) && !empty($value)) {
  505. return $value[0];
  506. }
  507. return false;
  508. }
  509. /**
  510. * @return string|false The entry's primary group Id
  511. * @throws ServerNotAvailableException
  512. */
  513. public function getGroupPrimaryGroupID(string $dn) {
  514. return $this->getEntryGroupID($dn, 'primaryGroupToken');
  515. }
  516. /**
  517. * @return string|false
  518. * @throws ServerNotAvailableException
  519. */
  520. public function getUserPrimaryGroupIDs(string $dn) {
  521. $primaryGroupID = false;
  522. if ($this->access->connection->hasPrimaryGroups) {
  523. $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
  524. if ($primaryGroupID === false) {
  525. $this->access->connection->hasPrimaryGroups = false;
  526. }
  527. }
  528. return $primaryGroupID;
  529. }
  530. /**
  531. * @throws Exception
  532. * @throws ServerNotAvailableException
  533. */
  534. private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
  535. $groupID = $this->getGroupPrimaryGroupID($groupDN);
  536. if ($groupID === false) {
  537. throw new Exception('Not a valid group');
  538. }
  539. $filterParts = [];
  540. $filterParts[] = $this->access->getFilterForUserCount();
  541. if ($search !== '') {
  542. $filterParts[] = $this->access->getFilterPartForUserSearch($search);
  543. }
  544. $filterParts[] = 'primaryGroupID=' . $groupID;
  545. return $this->access->combineFilterWithAnd($filterParts);
  546. }
  547. /**
  548. * @throws ServerNotAvailableException
  549. * @return array<int,string>
  550. */
  551. public function getUsersInPrimaryGroup(
  552. string $groupDN,
  553. string $search = '',
  554. ?int $limit = -1,
  555. ?int $offset = 0
  556. ): array {
  557. try {
  558. $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
  559. $users = $this->access->fetchListOfUsers(
  560. $filter,
  561. [$this->access->connection->ldapUserDisplayName, 'dn'],
  562. $limit,
  563. $offset
  564. );
  565. return $this->access->nextcloudUserNames($users);
  566. } catch (ServerNotAvailableException $e) {
  567. throw $e;
  568. } catch (Exception $e) {
  569. return [];
  570. }
  571. }
  572. /**
  573. * @throws ServerNotAvailableException
  574. */
  575. public function countUsersInPrimaryGroup(
  576. string $groupDN,
  577. string $search = '',
  578. int $limit = -1,
  579. int $offset = 0
  580. ): int {
  581. try {
  582. $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
  583. $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
  584. return (int)$users;
  585. } catch (ServerNotAvailableException $e) {
  586. throw $e;
  587. } catch (Exception $e) {
  588. return 0;
  589. }
  590. }
  591. /**
  592. * @return string|false
  593. * @throws ServerNotAvailableException
  594. */
  595. public function getUserPrimaryGroup(string $dn) {
  596. $groupID = $this->getUserPrimaryGroupIDs($dn);
  597. if ($groupID !== false) {
  598. $groupName = $this->primaryGroupID2Name($groupID, $dn);
  599. if ($groupName !== false) {
  600. return $groupName;
  601. }
  602. }
  603. return false;
  604. }
  605. private function isUserOnLDAP(string $uid): bool {
  606. // forces a user exists check - but does not help if a positive result is cached, while group info is not
  607. $ncUser = $this->ncUserManager->get($uid);
  608. if ($ncUser === null) {
  609. return false;
  610. }
  611. $backend = $ncUser->getBackend();
  612. if ($backend instanceof User_Proxy) {
  613. // ignoring cache as safeguard (and we are behind the group cache check anyway)
  614. return $backend->userExistsOnLDAP($uid, true);
  615. }
  616. return false;
  617. }
  618. protected function getCachedGroupsForUserId(string $uid): array {
  619. $groupStr = $this->config->getUserValue($uid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), '[]');
  620. return json_decode($groupStr) ?? [];
  621. }
  622. /**
  623. * This function fetches all groups a user belongs to. It does not check
  624. * if the user exists at all.
  625. *
  626. * This function includes groups based on dynamic group membership.
  627. *
  628. * @param string $uid Name of the user
  629. * @return string[] Group names
  630. * @throws Exception
  631. * @throws ServerNotAvailableException
  632. */
  633. public function getUserGroups($uid): array {
  634. if (!$this->enabled) {
  635. return [];
  636. }
  637. $ncUid = $uid;
  638. $cacheKey = 'getUserGroups' . $uid;
  639. $userGroups = $this->access->connection->getFromCache($cacheKey);
  640. if (!is_null($userGroups)) {
  641. return $userGroups;
  642. }
  643. $user = $this->access->userManager->get($uid);
  644. if ($user instanceof OfflineUser) {
  645. // We load known group memberships from configuration for remnants,
  646. // because LDAP server does not contain them anymore
  647. return $this->getCachedGroupsForUserId($uid);
  648. }
  649. $userDN = $this->access->username2dn($uid);
  650. if (!$userDN) {
  651. $this->access->connection->writeToCache($cacheKey, []);
  652. return [];
  653. }
  654. $groups = [];
  655. $primaryGroup = $this->getUserPrimaryGroup($userDN);
  656. $gidGroupName = $this->getUserGroupByGid($userDN);
  657. $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
  658. if (!empty($dynamicGroupMemberURL)) {
  659. // look through dynamic groups to add them to the result array if needed
  660. $groupsToMatch = $this->access->fetchListOfGroups(
  661. $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
  662. foreach ($groupsToMatch as $dynamicGroup) {
  663. if (!isset($dynamicGroup[$dynamicGroupMemberURL][0])) {
  664. continue;
  665. }
  666. $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
  667. if ($pos !== false) {
  668. $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
  669. // apply filter via ldap search to see if this user is in this
  670. // dynamic group
  671. $userMatch = $this->access->readAttribute(
  672. $userDN,
  673. $this->access->connection->ldapUserDisplayName,
  674. $memberUrlFilter
  675. );
  676. if ($userMatch !== false) {
  677. // match found so this user is in this group
  678. $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
  679. if (is_string($groupName)) {
  680. // be sure to never return false if the dn could not be
  681. // resolved to a name, for whatever reason.
  682. $groups[] = $groupName;
  683. }
  684. }
  685. } else {
  686. $this->logger->debug('No search filter found on member url of group {dn}',
  687. [
  688. 'app' => 'user_ldap',
  689. 'dn' => $dynamicGroup,
  690. ]
  691. );
  692. }
  693. }
  694. }
  695. // if possible, read out membership via memberOf. It's far faster than
  696. // performing a search, which still is a fallback later.
  697. // memberof doesn't support memberuid, so skip it here.
  698. if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
  699. && (int)$this->access->connection->useMemberOfToDetectMembership === 1
  700. && $this->ldapGroupMemberAssocAttr !== 'memberuid'
  701. && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
  702. $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
  703. foreach ($groupDNs as $dn) {
  704. $groupName = $this->access->dn2groupname($dn);
  705. if (is_string($groupName)) {
  706. // be sure to never return false if the dn could not be
  707. // resolved to a name, for whatever reason.
  708. $groups[] = $groupName;
  709. }
  710. }
  711. } else {
  712. // uniqueMember takes DN, memberuid the uid, so we need to distinguish
  713. switch ($this->ldapGroupMemberAssocAttr) {
  714. case 'uniquemember':
  715. case 'member':
  716. $uid = $userDN;
  717. break;
  718. case 'memberuid':
  719. case 'zimbramailforwardingaddress':
  720. $result = $this->access->readAttribute($userDN, 'uid');
  721. if ($result === false) {
  722. $this->logger->debug('No uid attribute found for DN {dn} on {host}',
  723. [
  724. 'app' => 'user_ldap',
  725. 'dn' => $userDN,
  726. 'host' => $this->access->connection->ldapHost,
  727. ]
  728. );
  729. $uid = false;
  730. } else {
  731. $uid = $result[0];
  732. }
  733. break;
  734. default:
  735. // just in case
  736. $uid = $userDN;
  737. break;
  738. }
  739. if ($uid !== false) {
  740. $groupsByMember = array_values($this->getGroupsByMember($uid));
  741. $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
  742. $groups = array_merge($groups, $groupsByMember);
  743. }
  744. }
  745. if ($primaryGroup !== false) {
  746. $groups[] = $primaryGroup;
  747. }
  748. if ($gidGroupName !== false) {
  749. $groups[] = $gidGroupName;
  750. }
  751. if (empty($groups) && !$this->isUserOnLDAP($ncUid)) {
  752. // Groups are enabled, but you user has none? Potentially suspicious:
  753. // it could be that the user was deleted from LDAP, but we are not
  754. // aware of it yet.
  755. $groups = $this->getCachedGroupsForUserId($ncUid);
  756. $this->access->connection->writeToCache($cacheKey, $groups);
  757. return $groups;
  758. }
  759. $groups = array_unique($groups, SORT_LOCALE_STRING);
  760. $this->access->connection->writeToCache($cacheKey, $groups);
  761. $groupStr = \json_encode($groups);
  762. $this->config->setUserValue($ncUid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), $groupStr);
  763. return $groups;
  764. }
  765. /**
  766. * @return array[]
  767. * @throws ServerNotAvailableException
  768. */
  769. private function getGroupsByMember(string $dn, array &$seen = []): array {
  770. if (isset($seen[$dn])) {
  771. return [];
  772. }
  773. $seen[$dn] = true;
  774. if (isset($this->cachedGroupsByMember[$dn])) {
  775. return $this->cachedGroupsByMember[$dn];
  776. }
  777. $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
  778. if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
  779. //in this case the member entries are email addresses
  780. $filter .= '@*';
  781. }
  782. $nesting = (int)$this->access->connection->ldapNestedGroups;
  783. if ($nesting === 0) {
  784. $filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
  785. }
  786. $allGroups = [];
  787. $groups = $this->access->fetchListOfGroups($filter,
  788. [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
  789. if ($nesting === 1) {
  790. while ($record = array_shift($groups)) {
  791. // Note: this has no effect when ldapGroupMemberAssocAttr is uid based
  792. $nestedParents = $this->getGroupsByMember($record['dn'][0], $seen);
  793. $groups = array_merge($groups, $nestedParents);
  794. $allGroups[] = $record;
  795. }
  796. } else {
  797. $allGroups = $groups;
  798. }
  799. $visibleGroups = $this->filterValidGroups($allGroups);
  800. $this->cachedGroupsByMember[$dn] = $visibleGroups;
  801. return $visibleGroups;
  802. }
  803. /**
  804. * get a list of all users in a group
  805. *
  806. * @param string $gid
  807. * @param string $search
  808. * @param int $limit
  809. * @param int $offset
  810. * @return array<int,string> user ids
  811. * @throws Exception
  812. * @throws ServerNotAvailableException
  813. */
  814. public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
  815. if (!$this->enabled) {
  816. return [];
  817. }
  818. if (!$this->groupExists($gid)) {
  819. return [];
  820. }
  821. $search = $this->access->escapeFilterPart($search, true);
  822. $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
  823. // check for cache of the exact query
  824. $groupUsers = $this->access->connection->getFromCache($cacheKey);
  825. if (!is_null($groupUsers)) {
  826. return $groupUsers;
  827. }
  828. if ($limit === -1) {
  829. $limit = null;
  830. }
  831. // check for cache of the query without limit and offset
  832. $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
  833. if (!is_null($groupUsers)) {
  834. $groupUsers = array_slice($groupUsers, $offset, $limit);
  835. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  836. return $groupUsers;
  837. }
  838. $groupDN = $this->access->groupname2dn($gid);
  839. if (!$groupDN) {
  840. // group couldn't be found, return empty result-set
  841. $this->access->connection->writeToCache($cacheKey, []);
  842. return [];
  843. }
  844. $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
  845. $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
  846. $members = $this->_groupMembers($groupDN);
  847. if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
  848. //in case users could not be retrieved, return empty result set
  849. $this->access->connection->writeToCache($cacheKey, []);
  850. return [];
  851. }
  852. $groupUsers = [];
  853. $attrs = $this->access->userManager->getAttributes(true);
  854. foreach ($members as $member) {
  855. switch ($this->ldapGroupMemberAssocAttr) {
  856. /** @noinspection PhpMissingBreakStatementInspection */
  857. case 'zimbramailforwardingaddress':
  858. //we get email addresses and need to convert them to uids
  859. $parts = explode('@', $member);
  860. $member = $parts[0];
  861. //no break needed because we just needed to remove the email part and now we have uids
  862. case 'memberuid':
  863. //we got uids, need to get their DNs to 'translate' them to user names
  864. $filter = $this->access->combineFilterWithAnd([
  865. str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
  866. $this->access->combineFilterWithAnd([
  867. $this->access->getFilterPartForUserSearch($search),
  868. $this->access->connection->ldapUserFilter
  869. ])
  870. ]);
  871. $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
  872. if (empty($ldap_users)) {
  873. break;
  874. }
  875. $uid = $this->access->dn2username($ldap_users[0]['dn'][0]);
  876. if (!$uid) {
  877. break;
  878. }
  879. $groupUsers[] = $uid;
  880. break;
  881. default:
  882. //we got DNs, check if we need to filter by search or we can give back all of them
  883. $uid = $this->access->dn2username($member);
  884. if (!$uid) {
  885. break;
  886. }
  887. $cacheKey = 'userExistsOnLDAP' . $uid;
  888. $userExists = $this->access->connection->getFromCache($cacheKey);
  889. if ($userExists === false) {
  890. break;
  891. }
  892. if ($userExists === null || $search !== '') {
  893. if (!$this->access->readAttribute($member,
  894. $this->access->connection->ldapUserDisplayName,
  895. $this->access->combineFilterWithAnd([
  896. $this->access->getFilterPartForUserSearch($search),
  897. $this->access->connection->ldapUserFilter
  898. ]))) {
  899. if ($search === '') {
  900. $this->access->connection->writeToCache($cacheKey, false);
  901. }
  902. break;
  903. }
  904. $this->access->connection->writeToCache($cacheKey, true);
  905. }
  906. $groupUsers[] = $uid;
  907. break;
  908. }
  909. }
  910. $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
  911. natsort($groupUsers);
  912. $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
  913. $groupUsers = array_slice($groupUsers, $offset, $limit);
  914. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  915. return $groupUsers;
  916. }
  917. /**
  918. * returns the number of users in a group, who match the search term
  919. *
  920. * @param string $gid the internal group name
  921. * @param string $search optional, a search string
  922. * @return int|bool
  923. * @throws Exception
  924. * @throws ServerNotAvailableException
  925. */
  926. public function countUsersInGroup($gid, $search = '') {
  927. if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
  928. return $this->groupPluginManager->countUsersInGroup($gid, $search);
  929. }
  930. $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
  931. if (!$this->enabled || !$this->groupExists($gid)) {
  932. return false;
  933. }
  934. $groupUsers = $this->access->connection->getFromCache($cacheKey);
  935. if (!is_null($groupUsers)) {
  936. return $groupUsers;
  937. }
  938. $groupDN = $this->access->groupname2dn($gid);
  939. if (!$groupDN) {
  940. // group couldn't be found, return empty result set
  941. $this->access->connection->writeToCache($cacheKey, false);
  942. return false;
  943. }
  944. $members = $this->_groupMembers($groupDN);
  945. $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
  946. if (!$members && $primaryUserCount === 0) {
  947. //in case users could not be retrieved, return empty result set
  948. $this->access->connection->writeToCache($cacheKey, false);
  949. return false;
  950. }
  951. if ($search === '') {
  952. $groupUsers = count($members) + $primaryUserCount;
  953. $this->access->connection->writeToCache($cacheKey, $groupUsers);
  954. return $groupUsers;
  955. }
  956. $search = $this->access->escapeFilterPart($search, true);
  957. $isMemberUid =
  958. ($this->ldapGroupMemberAssocAttr === 'memberuid' ||
  959. $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
  960. //we need to apply the search filter
  961. //alternatives that need to be checked:
  962. //a) get all users by search filter and array_intersect them
  963. //b) a, but only when less than 1k 10k ?k users like it is
  964. //c) put all DNs|uids in a LDAP filter, combine with the search string
  965. // and let it count.
  966. //For now this is not important, because the only use of this method
  967. //does not supply a search string
  968. $groupUsers = [];
  969. foreach ($members as $member) {
  970. if ($isMemberUid) {
  971. if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
  972. //we get email addresses and need to convert them to uids
  973. $parts = explode('@', $member);
  974. $member = $parts[0];
  975. }
  976. //we got uids, need to get their DNs to 'translate' them to user names
  977. $filter = $this->access->combineFilterWithAnd([
  978. str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
  979. $this->access->getFilterPartForUserSearch($search)
  980. ]);
  981. $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
  982. if (count($ldap_users) < 1) {
  983. continue;
  984. }
  985. $groupUsers[] = $this->access->dn2username($ldap_users[0]);
  986. } else {
  987. //we need to apply the search filter now
  988. if (!$this->access->readAttribute($member,
  989. $this->access->connection->ldapUserDisplayName,
  990. $this->access->getFilterPartForUserSearch($search))) {
  991. continue;
  992. }
  993. // dn2username will also check if the users belong to the allowed base
  994. if ($ncGroupId = $this->access->dn2username($member)) {
  995. $groupUsers[] = $ncGroupId;
  996. }
  997. }
  998. }
  999. //and get users that have the group as primary
  1000. $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
  1001. return count($groupUsers) + $primaryUsers;
  1002. }
  1003. /**
  1004. * get a list of all groups using a paged search
  1005. *
  1006. * @param string $search
  1007. * @param int $limit
  1008. * @param int $offset
  1009. * @return array with group names
  1010. *
  1011. * Returns a list with all groups
  1012. * Uses a paged search if available to override a
  1013. * server side search limit.
  1014. * (active directory has a limit of 1000 by default)
  1015. * @throws Exception
  1016. */
  1017. public function getGroups($search = '', $limit = -1, $offset = 0) {
  1018. if (!$this->enabled) {
  1019. return [];
  1020. }
  1021. $search = $this->access->escapeFilterPart($search, true);
  1022. $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
  1023. //Check cache before driving unnecessary searches
  1024. $ldap_groups = $this->access->connection->getFromCache($cacheKey);
  1025. if (!is_null($ldap_groups)) {
  1026. return $ldap_groups;
  1027. }
  1028. // if we'd pass -1 to LDAP search, we'd end up in a Protocol
  1029. // error. With a limit of 0, we get 0 results. So we pass null.
  1030. if ($limit <= 0) {
  1031. $limit = null;
  1032. }
  1033. $filter = $this->access->combineFilterWithAnd([
  1034. $this->access->connection->ldapGroupFilter,
  1035. $this->access->getFilterPartForGroupSearch($search)
  1036. ]);
  1037. $ldap_groups = $this->access->fetchListOfGroups($filter,
  1038. [$this->access->connection->ldapGroupDisplayName, 'dn'],
  1039. $limit,
  1040. $offset);
  1041. $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
  1042. $this->access->connection->writeToCache($cacheKey, $ldap_groups);
  1043. return $ldap_groups;
  1044. }
  1045. /**
  1046. * check if a group exists
  1047. *
  1048. * @param string $gid
  1049. * @return bool
  1050. * @throws ServerNotAvailableException
  1051. */
  1052. public function groupExists($gid) {
  1053. return $this->groupExistsOnLDAP($gid, false);
  1054. }
  1055. /**
  1056. * Check if a group exists
  1057. *
  1058. * @throws ServerNotAvailableException
  1059. */
  1060. public function groupExistsOnLDAP(string $gid, bool $ignoreCache = false): bool {
  1061. $cacheKey = 'groupExists' . $gid;
  1062. if (!$ignoreCache) {
  1063. $groupExists = $this->access->connection->getFromCache($cacheKey);
  1064. if (!is_null($groupExists)) {
  1065. return (bool)$groupExists;
  1066. }
  1067. }
  1068. //getting dn, if false the group does not exist. If dn, it may be mapped
  1069. //only, requires more checking.
  1070. $dn = $this->access->groupname2dn($gid);
  1071. if (!$dn) {
  1072. $this->access->connection->writeToCache($cacheKey, false);
  1073. return false;
  1074. }
  1075. if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
  1076. $this->access->connection->writeToCache($cacheKey, false);
  1077. return false;
  1078. }
  1079. //if group really still exists, we will be able to read its objectClass
  1080. if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
  1081. $this->access->connection->writeToCache($cacheKey, false);
  1082. return false;
  1083. }
  1084. $this->access->connection->writeToCache($cacheKey, true);
  1085. return true;
  1086. }
  1087. /**
  1088. * @template T
  1089. * @param array<array-key, T> $listOfGroups
  1090. * @return array<array-key, T>
  1091. * @throws ServerNotAvailableException
  1092. * @throws Exception
  1093. */
  1094. protected function filterValidGroups(array $listOfGroups): array {
  1095. $validGroupDNs = [];
  1096. foreach ($listOfGroups as $key => $item) {
  1097. $dn = is_string($item) ? $item : $item['dn'][0];
  1098. if (is_array($item) && !isset($item[$this->access->connection->ldapGroupDisplayName][0])) {
  1099. continue;
  1100. }
  1101. $name = $item[$this->access->connection->ldapGroupDisplayName][0] ?? null;
  1102. $gid = $this->access->dn2groupname($dn, $name);
  1103. if (!$gid) {
  1104. continue;
  1105. }
  1106. if ($this->groupExists($gid)) {
  1107. $validGroupDNs[$key] = $item;
  1108. }
  1109. }
  1110. return $validGroupDNs;
  1111. }
  1112. /**
  1113. * Check if backend implements actions
  1114. *
  1115. * @param int $actions bitwise-or'ed actions
  1116. * @return boolean
  1117. *
  1118. * Returns the supported actions as int to be
  1119. * compared with GroupInterface::CREATE_GROUP etc.
  1120. */
  1121. public function implementsActions($actions): bool {
  1122. return (bool)((GroupInterface::COUNT_USERS |
  1123. GroupInterface::DELETE_GROUP |
  1124. $this->groupPluginManager->getImplementedActions()) & $actions);
  1125. }
  1126. /**
  1127. * Return access for LDAP interaction.
  1128. *
  1129. * @return Access instance of Access for LDAP interaction
  1130. */
  1131. public function getLDAPAccess($gid) {
  1132. return $this->access;
  1133. }
  1134. /**
  1135. * create a group
  1136. *
  1137. * @param string $gid
  1138. * @return bool
  1139. * @throws Exception
  1140. * @throws ServerNotAvailableException
  1141. */
  1142. public function createGroup($gid) {
  1143. if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
  1144. if ($dn = $this->groupPluginManager->createGroup($gid)) {
  1145. //updates group mapping
  1146. $uuid = $this->access->getUUID($dn, false);
  1147. if (is_string($uuid)) {
  1148. $this->access->mapAndAnnounceIfApplicable(
  1149. $this->access->getGroupMapper(),
  1150. $dn,
  1151. $gid,
  1152. $uuid,
  1153. false
  1154. );
  1155. $this->access->cacheGroupExists($gid);
  1156. }
  1157. }
  1158. return $dn != null;
  1159. }
  1160. throw new Exception('Could not create group in LDAP backend.');
  1161. }
  1162. /**
  1163. * delete a group
  1164. *
  1165. * @param string $gid gid of the group to delete
  1166. * @throws Exception
  1167. */
  1168. public function deleteGroup(string $gid): bool {
  1169. if ($this->groupPluginManager->canDeleteGroup()) {
  1170. if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
  1171. // Delete group in nextcloud internal db
  1172. $this->access->getGroupMapper()->unmap($gid);
  1173. $this->access->connection->writeToCache("groupExists" . $gid, false);
  1174. }
  1175. return $ret;
  1176. }
  1177. // Getting dn, if false the group is not mapped
  1178. $dn = $this->access->groupname2dn($gid);
  1179. if (!$dn) {
  1180. throw new Exception('Could not delete unknown group '.$gid.' in LDAP backend.');
  1181. }
  1182. if (!$this->groupExists($gid)) {
  1183. // The group does not exist in the LDAP, remove the mapping
  1184. $this->access->getGroupMapper()->unmap($gid);
  1185. $this->access->connection->writeToCache("groupExists" . $gid, false);
  1186. return true;
  1187. }
  1188. throw new Exception('Could not delete existing group '.$gid.' in LDAP backend.');
  1189. }
  1190. /**
  1191. * Add a user to a group
  1192. *
  1193. * @param string $uid Name of the user to add to group
  1194. * @param string $gid Name of the group in which add the user
  1195. * @return bool
  1196. * @throws Exception
  1197. */
  1198. public function addToGroup($uid, $gid) {
  1199. if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
  1200. if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
  1201. $this->access->connection->clearCache();
  1202. unset($this->cachedGroupMembers[$gid]);
  1203. }
  1204. return $ret;
  1205. }
  1206. throw new Exception('Could not add user to group in LDAP backend.');
  1207. }
  1208. /**
  1209. * Removes a user from a group
  1210. *
  1211. * @param string $uid Name of the user to remove from group
  1212. * @param string $gid Name of the group from which remove the user
  1213. * @return bool
  1214. * @throws Exception
  1215. */
  1216. public function removeFromGroup($uid, $gid) {
  1217. if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
  1218. if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
  1219. $this->access->connection->clearCache();
  1220. unset($this->cachedGroupMembers[$gid]);
  1221. }
  1222. return $ret;
  1223. }
  1224. throw new Exception('Could not remove user from group in LDAP backend.');
  1225. }
  1226. /**
  1227. * Gets group details
  1228. *
  1229. * @param string $gid Name of the group
  1230. * @return array|false
  1231. * @throws Exception
  1232. */
  1233. public function getGroupDetails($gid) {
  1234. if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
  1235. return $this->groupPluginManager->getGroupDetails($gid);
  1236. }
  1237. throw new Exception('Could not get group details in LDAP backend.');
  1238. }
  1239. /**
  1240. * Return LDAP connection resource from a cloned connection.
  1241. * The cloned connection needs to be closed manually.
  1242. * of the current access.
  1243. *
  1244. * @param string $gid
  1245. * @return resource|\LDAP\Connection The LDAP connection
  1246. * @throws ServerNotAvailableException
  1247. */
  1248. public function getNewLDAPConnection($gid) {
  1249. $connection = clone $this->access->getConnection();
  1250. return $connection->getConnectionResource();
  1251. }
  1252. /**
  1253. * @throws ServerNotAvailableException
  1254. */
  1255. public function getDisplayName(string $gid): string {
  1256. if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
  1257. return $this->groupPluginManager->getDisplayName($gid);
  1258. }
  1259. $cacheKey = 'group_getDisplayName' . $gid;
  1260. if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
  1261. return $displayName;
  1262. }
  1263. $displayName = $this->access->readAttribute(
  1264. $this->access->groupname2dn($gid),
  1265. $this->access->connection->ldapGroupDisplayName);
  1266. if (($displayName !== false) && (count($displayName) > 0)) {
  1267. $displayName = $displayName[0];
  1268. } else {
  1269. $displayName = '';
  1270. }
  1271. $this->access->connection->writeToCache($cacheKey, $displayName);
  1272. return $displayName;
  1273. }
  1274. /**
  1275. * returns the groupname for the given LDAP DN, if available
  1276. */
  1277. public function dn2GroupName(string $dn): string|false {
  1278. return $this->access->dn2groupname($dn);
  1279. }
  1280. public function addRelationshipToCaches(string $uid, ?string $dnUser, string $gid): void {
  1281. $dnGroup = $this->access->groupname2dn($gid);
  1282. $dnUser ??= $this->access->username2dn($uid);
  1283. if ($dnUser === false || $dnGroup === false) {
  1284. return;
  1285. }
  1286. if (isset($this->cachedGroupMembers[$gid])) {
  1287. $this->cachedGroupMembers[$gid] = array_merge($this->cachedGroupMembers[$gid], [$dnUser]);
  1288. }
  1289. unset($this->cachedGroupsByMember[$dnUser]);
  1290. unset($this->cachedNestedGroups[$gid]);
  1291. $cacheKey = 'inGroup' . $uid . ':' . $gid;
  1292. $this->access->connection->writeToCache($cacheKey, true);
  1293. $cacheKeyMembers = 'inGroup-members:' . $gid;
  1294. if (!is_null($data = $this->access->connection->getFromCache($cacheKeyMembers))) {
  1295. $this->access->connection->writeToCache($cacheKeyMembers, array_merge($data, [$dnUser]));
  1296. }
  1297. $cacheKey = '_groupMembers' . $dnGroup;
  1298. if (!is_null($data = $this->access->connection->getFromCache($cacheKey))) {
  1299. $this->access->connection->writeToCache($cacheKey, array_merge($data, [$dnUser]));
  1300. }
  1301. $cacheKey = 'getUserGroups' . $uid;
  1302. if (!is_null($data = $this->access->connection->getFromCache($cacheKey))) {
  1303. $this->access->connection->writeToCache($cacheKey, array_merge($data, [$gid]));
  1304. }
  1305. // These cache keys cannot be easily updated:
  1306. // $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
  1307. // $cacheKey = 'usersInGroup-' . $gid . '-' . $search;
  1308. // $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
  1309. }
  1310. }