Group_LDAP.php 42 KB

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