@@ -54,1229 +54,1229 @@ |
||
54 | 54 | use OCP\ILogger; |
55 | 55 | |
56 | 56 | class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend { |
57 | - protected $enabled = false; |
|
58 | - |
|
59 | - /** @var string[] $cachedGroupMembers array of users with gid as key */ |
|
60 | - protected $cachedGroupMembers; |
|
61 | - /** @var string[] $cachedGroupsByMember array of groups with uid as key */ |
|
62 | - protected $cachedGroupsByMember; |
|
63 | - /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */ |
|
64 | - protected $cachedNestedGroups; |
|
65 | - /** @var GroupPluginManager */ |
|
66 | - protected $groupPluginManager; |
|
67 | - /** @var ILogger */ |
|
68 | - protected $logger; |
|
69 | - |
|
70 | - /** |
|
71 | - * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name |
|
72 | - */ |
|
73 | - protected $ldapGroupMemberAssocAttr; |
|
74 | - |
|
75 | - public function __construct(Access $access, GroupPluginManager $groupPluginManager) { |
|
76 | - parent::__construct($access); |
|
77 | - $filter = $this->access->connection->ldapGroupFilter; |
|
78 | - $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr; |
|
79 | - if (!empty($filter) && !empty($gAssoc)) { |
|
80 | - $this->enabled = true; |
|
81 | - } |
|
82 | - |
|
83 | - $this->cachedGroupMembers = new CappedMemoryCache(); |
|
84 | - $this->cachedGroupsByMember = new CappedMemoryCache(); |
|
85 | - $this->cachedNestedGroups = new CappedMemoryCache(); |
|
86 | - $this->groupPluginManager = $groupPluginManager; |
|
87 | - $this->logger = OC::$server->getLogger(); |
|
88 | - $this->ldapGroupMemberAssocAttr = strtolower($gAssoc); |
|
89 | - } |
|
90 | - |
|
91 | - /** |
|
92 | - * is user in group? |
|
93 | - * |
|
94 | - * @param string $uid uid of the user |
|
95 | - * @param string $gid gid of the group |
|
96 | - * @return bool |
|
97 | - * @throws Exception |
|
98 | - * @throws ServerNotAvailableException |
|
99 | - */ |
|
100 | - public function inGroup($uid, $gid) { |
|
101 | - if (!$this->enabled) { |
|
102 | - return false; |
|
103 | - } |
|
104 | - $cacheKey = 'inGroup' . $uid . ':' . $gid; |
|
105 | - $inGroup = $this->access->connection->getFromCache($cacheKey); |
|
106 | - if (!is_null($inGroup)) { |
|
107 | - return (bool)$inGroup; |
|
108 | - } |
|
109 | - |
|
110 | - $userDN = $this->access->username2dn($uid); |
|
111 | - |
|
112 | - if (isset($this->cachedGroupMembers[$gid])) { |
|
113 | - return in_array($userDN, $this->cachedGroupMembers[$gid]); |
|
114 | - } |
|
115 | - |
|
116 | - $cacheKeyMembers = 'inGroup-members:' . $gid; |
|
117 | - $members = $this->access->connection->getFromCache($cacheKeyMembers); |
|
118 | - if (!is_null($members)) { |
|
119 | - $this->cachedGroupMembers[$gid] = $members; |
|
120 | - $isInGroup = in_array($userDN, $members, true); |
|
121 | - $this->access->connection->writeToCache($cacheKey, $isInGroup); |
|
122 | - return $isInGroup; |
|
123 | - } |
|
124 | - |
|
125 | - $groupDN = $this->access->groupname2dn($gid); |
|
126 | - // just in case |
|
127 | - if (!$groupDN || !$userDN) { |
|
128 | - $this->access->connection->writeToCache($cacheKey, false); |
|
129 | - return false; |
|
130 | - } |
|
131 | - |
|
132 | - //check primary group first |
|
133 | - if ($gid === $this->getUserPrimaryGroup($userDN)) { |
|
134 | - $this->access->connection->writeToCache($cacheKey, true); |
|
135 | - return true; |
|
136 | - } |
|
137 | - |
|
138 | - //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course. |
|
139 | - $members = $this->_groupMembers($groupDN); |
|
140 | - if (!is_array($members) || count($members) === 0) { |
|
141 | - $this->access->connection->writeToCache($cacheKey, false); |
|
142 | - return false; |
|
143 | - } |
|
144 | - |
|
145 | - //extra work if we don't get back user DNs |
|
146 | - switch ($this->ldapGroupMemberAssocAttr) { |
|
147 | - case 'memberuid': |
|
148 | - case 'zimbramailforwardingaddress': |
|
149 | - $requestAttributes = $this->access->userManager->getAttributes(true); |
|
150 | - $dns = []; |
|
151 | - $filterParts = []; |
|
152 | - $bytes = 0; |
|
153 | - foreach ($members as $mid) { |
|
154 | - if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
155 | - $parts = explode('@', $mid); //making sure we get only the uid |
|
156 | - $mid = $parts[0]; |
|
157 | - } |
|
158 | - $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter); |
|
159 | - $filterParts[] = $filter; |
|
160 | - $bytes += strlen($filter); |
|
161 | - if ($bytes >= 9000000) { |
|
162 | - // AD has a default input buffer of 10 MB, we do not want |
|
163 | - // to take even the chance to exceed it |
|
164 | - $filter = $this->access->combineFilterWithOr($filterParts); |
|
165 | - $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts)); |
|
166 | - $bytes = 0; |
|
167 | - $filterParts = []; |
|
168 | - $dns = array_merge($dns, $users); |
|
169 | - } |
|
170 | - } |
|
171 | - if (count($filterParts) > 0) { |
|
172 | - $filter = $this->access->combineFilterWithOr($filterParts); |
|
173 | - $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts)); |
|
174 | - $dns = array_merge($dns, $users); |
|
175 | - } |
|
176 | - $members = $dns; |
|
177 | - break; |
|
178 | - } |
|
179 | - |
|
180 | - $isInGroup = in_array($userDN, $members); |
|
181 | - $this->access->connection->writeToCache($cacheKey, $isInGroup); |
|
182 | - $this->access->connection->writeToCache($cacheKeyMembers, $members); |
|
183 | - $this->cachedGroupMembers[$gid] = $members; |
|
184 | - |
|
185 | - return $isInGroup; |
|
186 | - } |
|
187 | - |
|
188 | - /** |
|
189 | - * For a group that has user membership defined by an LDAP search url |
|
190 | - * attribute returns the users that match the search url otherwise returns |
|
191 | - * an empty array. |
|
192 | - * |
|
193 | - * @throws ServerNotAvailableException |
|
194 | - */ |
|
195 | - public function getDynamicGroupMembers(string $dnGroup): array { |
|
196 | - $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL); |
|
197 | - |
|
198 | - if (empty($dynamicGroupMemberURL)) { |
|
199 | - return []; |
|
200 | - } |
|
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 | - /** |
|
233 | - * @throws ServerNotAvailableException |
|
234 | - */ |
|
235 | - private function _groupMembers(string $dnGroup, ?array &$seen = null): array { |
|
236 | - if ($seen === null) { |
|
237 | - $seen = []; |
|
238 | - } |
|
239 | - $allMembers = []; |
|
240 | - if (array_key_exists($dnGroup, $seen)) { |
|
241 | - return []; |
|
242 | - } |
|
243 | - // used extensively in cron job, caching makes sense for nested groups |
|
244 | - $cacheKey = '_groupMembers' . $dnGroup; |
|
245 | - $groupMembers = $this->access->connection->getFromCache($cacheKey); |
|
246 | - if ($groupMembers !== null) { |
|
247 | - return $groupMembers; |
|
248 | - } |
|
249 | - $seen[$dnGroup] = 1; |
|
250 | - $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr); |
|
251 | - if (is_array($members)) { |
|
252 | - $fetcher = function ($memberDN, &$seen) { |
|
253 | - return $this->_groupMembers($memberDN, $seen); |
|
254 | - }; |
|
255 | - $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members); |
|
256 | - } |
|
257 | - |
|
258 | - $allMembers += $this->getDynamicGroupMembers($dnGroup); |
|
259 | - |
|
260 | - $this->access->connection->writeToCache($cacheKey, $allMembers); |
|
261 | - return $allMembers; |
|
262 | - } |
|
263 | - |
|
264 | - /** |
|
265 | - * @throws ServerNotAvailableException |
|
266 | - */ |
|
267 | - private function _getGroupDNsFromMemberOf(string $dn): array { |
|
268 | - $groups = $this->access->readAttribute($dn, 'memberOf'); |
|
269 | - if (!is_array($groups)) { |
|
270 | - return []; |
|
271 | - } |
|
272 | - |
|
273 | - $fetcher = function ($groupDN) { |
|
274 | - if (isset($this->cachedNestedGroups[$groupDN])) { |
|
275 | - $nestedGroups = $this->cachedNestedGroups[$groupDN]; |
|
276 | - } else { |
|
277 | - $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf'); |
|
278 | - if (!is_array($nestedGroups)) { |
|
279 | - $nestedGroups = []; |
|
280 | - } |
|
281 | - $this->cachedNestedGroups[$groupDN] = $nestedGroups; |
|
282 | - } |
|
283 | - return $nestedGroups; |
|
284 | - }; |
|
285 | - |
|
286 | - $groups = $this->walkNestedGroups($dn, $fetcher, $groups); |
|
287 | - return $this->filterValidGroups($groups); |
|
288 | - } |
|
289 | - |
|
290 | - private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array { |
|
291 | - $nesting = (int)$this->access->connection->ldapNestedGroups; |
|
292 | - // depending on the input, we either have a list of DNs or a list of LDAP records |
|
293 | - // also, the output expects either DNs or records. Testing the first element should suffice. |
|
294 | - $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]); |
|
295 | - |
|
296 | - if ($nesting !== 1) { |
|
297 | - if ($recordMode) { |
|
298 | - // the keys are numeric, but should hold the DN |
|
299 | - return array_reduce($list, function ($transformed, $record) use ($dn) { |
|
300 | - if ($record['dn'][0] != $dn) { |
|
301 | - $transformed[$record['dn'][0]] = $record; |
|
302 | - } |
|
303 | - return $transformed; |
|
304 | - }, []); |
|
305 | - } |
|
306 | - return $list; |
|
307 | - } |
|
308 | - |
|
309 | - $seen = []; |
|
310 | - while ($record = array_pop($list)) { |
|
311 | - $recordDN = $recordMode ? $record['dn'][0] : $record; |
|
312 | - if ($recordDN === $dn || array_key_exists($recordDN, $seen)) { |
|
313 | - // Prevent loops |
|
314 | - continue; |
|
315 | - } |
|
316 | - $fetched = $fetcher($record, $seen); |
|
317 | - $list = array_merge($list, $fetched); |
|
318 | - $seen[$recordDN] = $record; |
|
319 | - } |
|
320 | - |
|
321 | - return $recordMode ? $seen : array_keys($seen); |
|
322 | - } |
|
323 | - |
|
324 | - /** |
|
325 | - * translates a gidNumber into an ownCloud internal name |
|
326 | - * |
|
327 | - * @return string|bool |
|
328 | - * @throws Exception |
|
329 | - * @throws ServerNotAvailableException |
|
330 | - */ |
|
331 | - public function gidNumber2Name(string $gid, string $dn) { |
|
332 | - $cacheKey = 'gidNumberToName' . $gid; |
|
333 | - $groupName = $this->access->connection->getFromCache($cacheKey); |
|
334 | - if (!is_null($groupName) && isset($groupName)) { |
|
335 | - return $groupName; |
|
336 | - } |
|
337 | - |
|
338 | - //we need to get the DN from LDAP |
|
339 | - $filter = $this->access->combineFilterWithAnd([ |
|
340 | - $this->access->connection->ldapGroupFilter, |
|
341 | - 'objectClass=posixGroup', |
|
342 | - $this->access->connection->ldapGidNumber . '=' . $gid |
|
343 | - ]); |
|
344 | - return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
|
345 | - } |
|
346 | - |
|
347 | - /** |
|
348 | - * @throws ServerNotAvailableException |
|
349 | - * @throws Exception |
|
350 | - */ |
|
351 | - private function getNameOfGroup(string $filter, string $cacheKey) { |
|
352 | - $result = $this->access->searchGroups($filter, ['dn'], 1); |
|
353 | - if (empty($result)) { |
|
354 | - return null; |
|
355 | - } |
|
356 | - $dn = $result[0]['dn'][0]; |
|
357 | - |
|
358 | - //and now the group name |
|
359 | - //NOTE once we have separate Nextcloud group IDs and group names we can |
|
360 | - //directly read the display name attribute instead of the DN |
|
361 | - $name = $this->access->dn2groupname($dn); |
|
362 | - |
|
363 | - $this->access->connection->writeToCache($cacheKey, $name); |
|
364 | - |
|
365 | - return $name; |
|
366 | - } |
|
367 | - |
|
368 | - /** |
|
369 | - * returns the entry's gidNumber |
|
370 | - * |
|
371 | - * @return string|bool |
|
372 | - * @throws ServerNotAvailableException |
|
373 | - */ |
|
374 | - private function getEntryGidNumber(string $dn, string $attribute) { |
|
375 | - $value = $this->access->readAttribute($dn, $attribute); |
|
376 | - if (is_array($value) && !empty($value)) { |
|
377 | - return $value[0]; |
|
378 | - } |
|
379 | - return false; |
|
380 | - } |
|
381 | - |
|
382 | - /** |
|
383 | - * @return string|bool |
|
384 | - * @throws ServerNotAvailableException |
|
385 | - */ |
|
386 | - public function getGroupGidNumber(string $dn) { |
|
387 | - return $this->getEntryGidNumber($dn, 'gidNumber'); |
|
388 | - } |
|
389 | - |
|
390 | - /** |
|
391 | - * returns the user's gidNumber |
|
392 | - * |
|
393 | - * @return string|bool |
|
394 | - * @throws ServerNotAvailableException |
|
395 | - */ |
|
396 | - public function getUserGidNumber(string $dn) { |
|
397 | - $gidNumber = false; |
|
398 | - if ($this->access->connection->hasGidNumber) { |
|
399 | - $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber); |
|
400 | - if ($gidNumber === false) { |
|
401 | - $this->access->connection->hasGidNumber = false; |
|
402 | - } |
|
403 | - } |
|
404 | - return $gidNumber; |
|
405 | - } |
|
406 | - |
|
407 | - /** |
|
408 | - * @throws ServerNotAvailableException |
|
409 | - * @throws Exception |
|
410 | - */ |
|
411 | - private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string { |
|
412 | - $groupID = $this->getGroupGidNumber($groupDN); |
|
413 | - if ($groupID === false) { |
|
414 | - throw new Exception('Not a valid group'); |
|
415 | - } |
|
416 | - |
|
417 | - $filterParts = []; |
|
418 | - $filterParts[] = $this->access->getFilterForUserCount(); |
|
419 | - if ($search !== '') { |
|
420 | - $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
|
421 | - } |
|
422 | - $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID; |
|
423 | - |
|
424 | - return $this->access->combineFilterWithAnd($filterParts); |
|
425 | - } |
|
426 | - |
|
427 | - /** |
|
428 | - * returns a list of users that have the given group as gid number |
|
429 | - * |
|
430 | - * @throws ServerNotAvailableException |
|
431 | - */ |
|
432 | - public function getUsersInGidNumber( |
|
433 | - string $groupDN, |
|
434 | - string $search = '', |
|
435 | - ?int $limit = -1, |
|
436 | - ?int $offset = 0 |
|
437 | - ): array { |
|
438 | - try { |
|
439 | - $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search); |
|
440 | - $users = $this->access->fetchListOfUsers( |
|
441 | - $filter, |
|
442 | - [$this->access->connection->ldapUserDisplayName, 'dn'], |
|
443 | - $limit, |
|
444 | - $offset |
|
445 | - ); |
|
446 | - return $this->access->nextcloudUserNames($users); |
|
447 | - } catch (ServerNotAvailableException $e) { |
|
448 | - throw $e; |
|
449 | - } catch (Exception $e) { |
|
450 | - return []; |
|
451 | - } |
|
452 | - } |
|
453 | - |
|
454 | - /** |
|
455 | - * @throws ServerNotAvailableException |
|
456 | - * @return bool |
|
457 | - */ |
|
458 | - public function getUserGroupByGid(string $dn) { |
|
459 | - $groupID = $this->getUserGidNumber($dn); |
|
460 | - if ($groupID !== false) { |
|
461 | - $groupName = $this->gidNumber2Name($groupID, $dn); |
|
462 | - if ($groupName !== false) { |
|
463 | - return $groupName; |
|
464 | - } |
|
465 | - } |
|
466 | - |
|
467 | - return false; |
|
468 | - } |
|
469 | - |
|
470 | - /** |
|
471 | - * translates a primary group ID into an Nextcloud internal name |
|
472 | - * |
|
473 | - * @return string|bool |
|
474 | - * @throws Exception |
|
475 | - * @throws ServerNotAvailableException |
|
476 | - */ |
|
477 | - public function primaryGroupID2Name(string $gid, string $dn) { |
|
478 | - $cacheKey = 'primaryGroupIDtoName'; |
|
479 | - $groupNames = $this->access->connection->getFromCache($cacheKey); |
|
480 | - if (!is_null($groupNames) && isset($groupNames[$gid])) { |
|
481 | - return $groupNames[$gid]; |
|
482 | - } |
|
483 | - |
|
484 | - $domainObjectSid = $this->access->getSID($dn); |
|
485 | - if ($domainObjectSid === false) { |
|
486 | - return false; |
|
487 | - } |
|
488 | - |
|
489 | - //we need to get the DN from LDAP |
|
490 | - $filter = $this->access->combineFilterWithAnd([ |
|
491 | - $this->access->connection->ldapGroupFilter, |
|
492 | - 'objectsid=' . $domainObjectSid . '-' . $gid |
|
493 | - ]); |
|
494 | - return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
|
495 | - } |
|
496 | - |
|
497 | - /** |
|
498 | - * returns the entry's primary group ID |
|
499 | - * |
|
500 | - * @return string|bool |
|
501 | - * @throws ServerNotAvailableException |
|
502 | - */ |
|
503 | - private function getEntryGroupID(string $dn, string $attribute) { |
|
504 | - $value = $this->access->readAttribute($dn, $attribute); |
|
505 | - if (is_array($value) && !empty($value)) { |
|
506 | - return $value[0]; |
|
507 | - } |
|
508 | - return false; |
|
509 | - } |
|
510 | - |
|
511 | - /** |
|
512 | - * @return string|bool |
|
513 | - * @throws ServerNotAvailableException |
|
514 | - */ |
|
515 | - public function getGroupPrimaryGroupID(string $dn) { |
|
516 | - return $this->getEntryGroupID($dn, 'primaryGroupToken'); |
|
517 | - } |
|
518 | - |
|
519 | - /** |
|
520 | - * @return string|bool |
|
521 | - * @throws ServerNotAvailableException |
|
522 | - */ |
|
523 | - public function getUserPrimaryGroupIDs(string $dn) { |
|
524 | - $primaryGroupID = false; |
|
525 | - if ($this->access->connection->hasPrimaryGroups) { |
|
526 | - $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID'); |
|
527 | - if ($primaryGroupID === false) { |
|
528 | - $this->access->connection->hasPrimaryGroups = false; |
|
529 | - } |
|
530 | - } |
|
531 | - return $primaryGroupID; |
|
532 | - } |
|
533 | - |
|
534 | - /** |
|
535 | - * @throws Exception |
|
536 | - * @throws ServerNotAvailableException |
|
537 | - */ |
|
538 | - private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string { |
|
539 | - $groupID = $this->getGroupPrimaryGroupID($groupDN); |
|
540 | - if ($groupID === false) { |
|
541 | - throw new Exception('Not a valid group'); |
|
542 | - } |
|
543 | - |
|
544 | - $filterParts = []; |
|
545 | - $filterParts[] = $this->access->getFilterForUserCount(); |
|
546 | - if ($search !== '') { |
|
547 | - $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
|
548 | - } |
|
549 | - $filterParts[] = 'primaryGroupID=' . $groupID; |
|
550 | - |
|
551 | - return $this->access->combineFilterWithAnd($filterParts); |
|
552 | - } |
|
553 | - |
|
554 | - /** |
|
555 | - * @throws ServerNotAvailableException |
|
556 | - */ |
|
557 | - public function getUsersInPrimaryGroup( |
|
558 | - string $groupDN, |
|
559 | - string $search = '', |
|
560 | - ?int $limit = -1, |
|
561 | - ?int $offset = 0 |
|
562 | - ): array { |
|
563 | - try { |
|
564 | - $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); |
|
565 | - $users = $this->access->fetchListOfUsers( |
|
566 | - $filter, |
|
567 | - [$this->access->connection->ldapUserDisplayName, 'dn'], |
|
568 | - $limit, |
|
569 | - $offset |
|
570 | - ); |
|
571 | - return $this->access->nextcloudUserNames($users); |
|
572 | - } catch (ServerNotAvailableException $e) { |
|
573 | - throw $e; |
|
574 | - } catch (Exception $e) { |
|
575 | - return []; |
|
576 | - } |
|
577 | - } |
|
578 | - |
|
579 | - /** |
|
580 | - * @throws ServerNotAvailableException |
|
581 | - */ |
|
582 | - public function countUsersInPrimaryGroup( |
|
583 | - string $groupDN, |
|
584 | - string $search = '', |
|
585 | - int $limit = -1, |
|
586 | - int $offset = 0 |
|
587 | - ): int { |
|
588 | - try { |
|
589 | - $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); |
|
590 | - $users = $this->access->countUsers($filter, ['dn'], $limit, $offset); |
|
591 | - return (int)$users; |
|
592 | - } catch (ServerNotAvailableException $e) { |
|
593 | - throw $e; |
|
594 | - } catch (Exception $e) { |
|
595 | - return 0; |
|
596 | - } |
|
597 | - } |
|
598 | - |
|
599 | - /** |
|
600 | - * @return string|bool |
|
601 | - * @throws ServerNotAvailableException |
|
602 | - */ |
|
603 | - public function getUserPrimaryGroup(string $dn) { |
|
604 | - $groupID = $this->getUserPrimaryGroupIDs($dn); |
|
605 | - if ($groupID !== false) { |
|
606 | - $groupName = $this->primaryGroupID2Name($groupID, $dn); |
|
607 | - if ($groupName !== false) { |
|
608 | - return $groupName; |
|
609 | - } |
|
610 | - } |
|
611 | - |
|
612 | - return false; |
|
613 | - } |
|
614 | - |
|
615 | - /** |
|
616 | - * This function fetches all groups a user belongs to. It does not check |
|
617 | - * if the user exists at all. |
|
618 | - * |
|
619 | - * This function includes groups based on dynamic group membership. |
|
620 | - * |
|
621 | - * @param string $uid Name of the user |
|
622 | - * @return array with group names |
|
623 | - * @throws Exception |
|
624 | - * @throws ServerNotAvailableException |
|
625 | - */ |
|
626 | - public function getUserGroups($uid) { |
|
627 | - if (!$this->enabled) { |
|
628 | - return []; |
|
629 | - } |
|
630 | - $cacheKey = 'getUserGroups' . $uid; |
|
631 | - $userGroups = $this->access->connection->getFromCache($cacheKey); |
|
632 | - if (!is_null($userGroups)) { |
|
633 | - return $userGroups; |
|
634 | - } |
|
635 | - $userDN = $this->access->username2dn($uid); |
|
636 | - if (!$userDN) { |
|
637 | - $this->access->connection->writeToCache($cacheKey, []); |
|
638 | - return []; |
|
639 | - } |
|
640 | - |
|
641 | - $groups = []; |
|
642 | - $primaryGroup = $this->getUserPrimaryGroup($userDN); |
|
643 | - $gidGroupName = $this->getUserGroupByGid($userDN); |
|
644 | - |
|
645 | - $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL); |
|
646 | - |
|
647 | - if (!empty($dynamicGroupMemberURL)) { |
|
648 | - // look through dynamic groups to add them to the result array if needed |
|
649 | - $groupsToMatch = $this->access->fetchListOfGroups( |
|
650 | - $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]); |
|
651 | - foreach ($groupsToMatch as $dynamicGroup) { |
|
652 | - if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) { |
|
653 | - continue; |
|
654 | - } |
|
655 | - $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '('); |
|
656 | - if ($pos !== false) { |
|
657 | - $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos); |
|
658 | - // apply filter via ldap search to see if this user is in this |
|
659 | - // dynamic group |
|
660 | - $userMatch = $this->access->readAttribute( |
|
661 | - $userDN, |
|
662 | - $this->access->connection->ldapUserDisplayName, |
|
663 | - $memberUrlFilter |
|
664 | - ); |
|
665 | - if ($userMatch !== false) { |
|
666 | - // match found so this user is in this group |
|
667 | - $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]); |
|
668 | - if (is_string($groupName)) { |
|
669 | - // be sure to never return false if the dn could not be |
|
670 | - // resolved to a name, for whatever reason. |
|
671 | - $groups[] = $groupName; |
|
672 | - } |
|
673 | - } |
|
674 | - } else { |
|
675 | - $this->logger->debug('No search filter found on member url of group {dn}', |
|
676 | - [ |
|
677 | - 'app' => 'user_ldap', |
|
678 | - 'dn' => $dynamicGroup, |
|
679 | - ] |
|
680 | - ); |
|
681 | - } |
|
682 | - } |
|
683 | - } |
|
684 | - |
|
685 | - // if possible, read out membership via memberOf. It's far faster than |
|
686 | - // performing a search, which still is a fallback later. |
|
687 | - // memberof doesn't support memberuid, so skip it here. |
|
688 | - if ((int)$this->access->connection->hasMemberOfFilterSupport === 1 |
|
689 | - && (int)$this->access->connection->useMemberOfToDetectMembership === 1 |
|
690 | - && $this->ldapGroupMemberAssocAttr !== 'memberuid' |
|
691 | - && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') { |
|
692 | - $groupDNs = $this->_getGroupDNsFromMemberOf($userDN); |
|
693 | - if (is_array($groupDNs)) { |
|
694 | - foreach ($groupDNs as $dn) { |
|
695 | - $groupName = $this->access->dn2groupname($dn); |
|
696 | - if (is_string($groupName)) { |
|
697 | - // be sure to never return false if the dn could not be |
|
698 | - // resolved to a name, for whatever reason. |
|
699 | - $groups[] = $groupName; |
|
700 | - } |
|
701 | - } |
|
702 | - } |
|
703 | - |
|
704 | - if ($primaryGroup !== false) { |
|
705 | - $groups[] = $primaryGroup; |
|
706 | - } |
|
707 | - if ($gidGroupName !== false) { |
|
708 | - $groups[] = $gidGroupName; |
|
709 | - } |
|
710 | - $this->access->connection->writeToCache($cacheKey, $groups); |
|
711 | - return $groups; |
|
712 | - } |
|
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 | - |
|
721 | - case 'memberuid': |
|
722 | - case 'zimbramailforwardingaddress': |
|
723 | - $result = $this->access->readAttribute($userDN, 'uid'); |
|
724 | - if ($result === false) { |
|
725 | - $this->logger->debug('No uid attribute found for DN {dn} on {host}', |
|
726 | - [ |
|
727 | - 'app' => 'user_ldap', |
|
728 | - 'dn' => $userDN, |
|
729 | - 'host' => $this->access->connection->ldapHost, |
|
730 | - ] |
|
731 | - ); |
|
732 | - $uid = false; |
|
733 | - } else { |
|
734 | - $uid = $result[0]; |
|
735 | - } |
|
736 | - break; |
|
737 | - |
|
738 | - default: |
|
739 | - // just in case |
|
740 | - $uid = $userDN; |
|
741 | - break; |
|
742 | - } |
|
743 | - |
|
744 | - if ($uid !== false) { |
|
745 | - if (isset($this->cachedGroupsByMember[$uid])) { |
|
746 | - $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]); |
|
747 | - } else { |
|
748 | - $groupsByMember = array_values($this->getGroupsByMember($uid)); |
|
749 | - $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember); |
|
750 | - $this->cachedGroupsByMember[$uid] = $groupsByMember; |
|
751 | - $groups = array_merge($groups, $groupsByMember); |
|
752 | - } |
|
753 | - } |
|
754 | - |
|
755 | - if ($primaryGroup !== false) { |
|
756 | - $groups[] = $primaryGroup; |
|
757 | - } |
|
758 | - if ($gidGroupName !== false) { |
|
759 | - $groups[] = $gidGroupName; |
|
760 | - } |
|
761 | - |
|
762 | - $groups = array_unique($groups, SORT_LOCALE_STRING); |
|
763 | - $this->access->connection->writeToCache($cacheKey, $groups); |
|
764 | - |
|
765 | - return $groups; |
|
766 | - } |
|
767 | - |
|
768 | - /** |
|
769 | - * @throws ServerNotAvailableException |
|
770 | - */ |
|
771 | - private function getGroupsByMember(string $dn, array &$seen = null): array { |
|
772 | - if ($seen === null) { |
|
773 | - $seen = []; |
|
774 | - } |
|
775 | - if (array_key_exists($dn, $seen)) { |
|
776 | - // avoid loops |
|
777 | - return []; |
|
778 | - } |
|
779 | - $allGroups = []; |
|
780 | - $seen[$dn] = true; |
|
781 | - $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn; |
|
782 | - |
|
783 | - if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
784 | - //in this case the member entries are email addresses |
|
785 | - $filter .= '@*'; |
|
786 | - } |
|
787 | - |
|
788 | - $groups = $this->access->fetchListOfGroups($filter, |
|
789 | - [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']); |
|
790 | - if (is_array($groups)) { |
|
791 | - $fetcher = function ($dn, &$seen) { |
|
792 | - if (is_array($dn) && isset($dn['dn'][0])) { |
|
793 | - $dn = $dn['dn'][0]; |
|
794 | - } |
|
795 | - return $this->getGroupsByMember($dn, $seen); |
|
796 | - }; |
|
797 | - |
|
798 | - if (empty($dn)) { |
|
799 | - $dn = ""; |
|
800 | - } |
|
801 | - |
|
802 | - $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups); |
|
803 | - } |
|
804 | - $visibleGroups = $this->filterValidGroups($allGroups); |
|
805 | - return array_intersect_key($allGroups, $visibleGroups); |
|
806 | - } |
|
807 | - |
|
808 | - /** |
|
809 | - * get a list of all users in a group |
|
810 | - * |
|
811 | - * @param string $gid |
|
812 | - * @param string $search |
|
813 | - * @param int $limit |
|
814 | - * @param int $offset |
|
815 | - * @return array with user ids |
|
816 | - * @throws Exception |
|
817 | - * @throws ServerNotAvailableException |
|
818 | - */ |
|
819 | - public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { |
|
820 | - if (!$this->enabled) { |
|
821 | - return []; |
|
822 | - } |
|
823 | - if (!$this->groupExists($gid)) { |
|
824 | - return []; |
|
825 | - } |
|
826 | - $search = $this->access->escapeFilterPart($search, true); |
|
827 | - $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset; |
|
828 | - // check for cache of the exact query |
|
829 | - $groupUsers = $this->access->connection->getFromCache($cacheKey); |
|
830 | - if (!is_null($groupUsers)) { |
|
831 | - return $groupUsers; |
|
832 | - } |
|
833 | - |
|
834 | - if ($limit === -1) { |
|
835 | - $limit = null; |
|
836 | - } |
|
837 | - // check for cache of the query without limit and offset |
|
838 | - $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search); |
|
839 | - if (!is_null($groupUsers)) { |
|
840 | - $groupUsers = array_slice($groupUsers, $offset, $limit); |
|
841 | - $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
842 | - return $groupUsers; |
|
843 | - } |
|
844 | - |
|
845 | - $groupDN = $this->access->groupname2dn($gid); |
|
846 | - if (!$groupDN) { |
|
847 | - // group couldn't be found, return empty resultset |
|
848 | - $this->access->connection->writeToCache($cacheKey, []); |
|
849 | - return []; |
|
850 | - } |
|
851 | - |
|
852 | - $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset); |
|
853 | - $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset); |
|
854 | - $members = $this->_groupMembers($groupDN); |
|
855 | - if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) { |
|
856 | - //in case users could not be retrieved, return empty result set |
|
857 | - $this->access->connection->writeToCache($cacheKey, []); |
|
858 | - return []; |
|
859 | - } |
|
860 | - |
|
861 | - $groupUsers = []; |
|
862 | - $attrs = $this->access->userManager->getAttributes(true); |
|
863 | - foreach ($members as $member) { |
|
864 | - switch ($this->ldapGroupMemberAssocAttr) { |
|
865 | - case 'zimbramailforwardingaddress': |
|
866 | - //we get email addresses and need to convert them to uids |
|
867 | - $parts = explode('@', $member); |
|
868 | - $member = $parts[0]; |
|
869 | - //no break needed because we just needed to remove the email part and now we have uids |
|
870 | - case 'memberuid': |
|
871 | - //we got uids, need to get their DNs to 'translate' them to user names |
|
872 | - $filter = $this->access->combineFilterWithAnd([ |
|
873 | - str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter), |
|
874 | - $this->access->combineFilterWithAnd([ |
|
875 | - $this->access->getFilterPartForUserSearch($search), |
|
876 | - $this->access->connection->ldapUserFilter |
|
877 | - ]) |
|
878 | - ]); |
|
879 | - $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1); |
|
880 | - if (count($ldap_users) < 1) { |
|
881 | - continue; |
|
882 | - } |
|
883 | - $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]); |
|
884 | - break; |
|
885 | - default: |
|
886 | - //we got DNs, check if we need to filter by search or we can give back all of them |
|
887 | - $uid = $this->access->dn2username($member); |
|
888 | - if (!$uid) { |
|
889 | - continue; |
|
890 | - } |
|
891 | - |
|
892 | - $cacheKey = 'userExistsOnLDAP' . $uid; |
|
893 | - $userExists = $this->access->connection->getFromCache($cacheKey); |
|
894 | - if ($userExists === false) { |
|
895 | - continue; |
|
896 | - } |
|
897 | - if ($userExists === null || $search !== '') { |
|
898 | - if (!$this->access->readAttribute($member, |
|
899 | - $this->access->connection->ldapUserDisplayName, |
|
900 | - $this->access->combineFilterWithAnd([ |
|
901 | - $this->access->getFilterPartForUserSearch($search), |
|
902 | - $this->access->connection->ldapUserFilter |
|
903 | - ]))) { |
|
904 | - if ($search === '') { |
|
905 | - $this->access->connection->writeToCache($cacheKey, false); |
|
906 | - } |
|
907 | - continue; |
|
908 | - } |
|
909 | - $this->access->connection->writeToCache($cacheKey, true); |
|
910 | - } |
|
911 | - $groupUsers[] = $uid; |
|
912 | - break; |
|
913 | - } |
|
914 | - } |
|
915 | - |
|
916 | - $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers)); |
|
917 | - natsort($groupUsers); |
|
918 | - $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers); |
|
919 | - $groupUsers = array_slice($groupUsers, $offset, $limit); |
|
920 | - |
|
921 | - $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
922 | - |
|
923 | - return $groupUsers; |
|
924 | - } |
|
925 | - |
|
926 | - /** |
|
927 | - * returns the number of users in a group, who match the search term |
|
928 | - * |
|
929 | - * @param string $gid the internal group name |
|
930 | - * @param string $search optional, a search string |
|
931 | - * @return int|bool |
|
932 | - * @throws Exception |
|
933 | - * @throws ServerNotAvailableException |
|
934 | - */ |
|
935 | - public function countUsersInGroup($gid, $search = '') { |
|
936 | - if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) { |
|
937 | - return $this->groupPluginManager->countUsersInGroup($gid, $search); |
|
938 | - } |
|
939 | - |
|
940 | - $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search; |
|
941 | - if (!$this->enabled || !$this->groupExists($gid)) { |
|
942 | - return false; |
|
943 | - } |
|
944 | - $groupUsers = $this->access->connection->getFromCache($cacheKey); |
|
945 | - if (!is_null($groupUsers)) { |
|
946 | - return $groupUsers; |
|
947 | - } |
|
948 | - |
|
949 | - $groupDN = $this->access->groupname2dn($gid); |
|
950 | - if (!$groupDN) { |
|
951 | - // group couldn't be found, return empty result set |
|
952 | - $this->access->connection->writeToCache($cacheKey, false); |
|
953 | - return false; |
|
954 | - } |
|
955 | - |
|
956 | - $members = $this->_groupMembers($groupDN); |
|
957 | - $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, ''); |
|
958 | - if (!$members && $primaryUserCount === 0) { |
|
959 | - //in case users could not be retrieved, return empty result set |
|
960 | - $this->access->connection->writeToCache($cacheKey, false); |
|
961 | - return false; |
|
962 | - } |
|
963 | - |
|
964 | - if ($search === '') { |
|
965 | - $groupUsers = count($members) + $primaryUserCount; |
|
966 | - $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
967 | - return $groupUsers; |
|
968 | - } |
|
969 | - $search = $this->access->escapeFilterPart($search, true); |
|
970 | - $isMemberUid = |
|
971 | - ($this->ldapGroupMemberAssocAttr === 'memberuid' || |
|
972 | - $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress'); |
|
973 | - |
|
974 | - //we need to apply the search filter |
|
975 | - //alternatives that need to be checked: |
|
976 | - //a) get all users by search filter and array_intersect them |
|
977 | - //b) a, but only when less than 1k 10k ?k users like it is |
|
978 | - //c) put all DNs|uids in a LDAP filter, combine with the search string |
|
979 | - // and let it count. |
|
980 | - //For now this is not important, because the only use of this method |
|
981 | - //does not supply a search string |
|
982 | - $groupUsers = []; |
|
983 | - foreach ($members as $member) { |
|
984 | - if ($isMemberUid) { |
|
985 | - if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
986 | - //we get email addresses and need to convert them to uids |
|
987 | - $parts = explode('@', $member); |
|
988 | - $member = $parts[0]; |
|
989 | - } |
|
990 | - //we got uids, need to get their DNs to 'translate' them to user names |
|
991 | - $filter = $this->access->combineFilterWithAnd([ |
|
992 | - str_replace('%uid', $member, $this->access->connection->ldapLoginFilter), |
|
993 | - $this->access->getFilterPartForUserSearch($search) |
|
994 | - ]); |
|
995 | - $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1); |
|
996 | - if (count($ldap_users) < 1) { |
|
997 | - continue; |
|
998 | - } |
|
999 | - $groupUsers[] = $this->access->dn2username($ldap_users[0]); |
|
1000 | - } else { |
|
1001 | - //we need to apply the search filter now |
|
1002 | - if (!$this->access->readAttribute($member, |
|
1003 | - $this->access->connection->ldapUserDisplayName, |
|
1004 | - $this->access->getFilterPartForUserSearch($search))) { |
|
1005 | - continue; |
|
1006 | - } |
|
1007 | - // dn2username will also check if the users belong to the allowed base |
|
1008 | - if ($ncGroupId = $this->access->dn2username($member)) { |
|
1009 | - $groupUsers[] = $ncGroupId; |
|
1010 | - } |
|
1011 | - } |
|
1012 | - } |
|
1013 | - |
|
1014 | - //and get users that have the group as primary |
|
1015 | - $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search); |
|
1016 | - |
|
1017 | - return count($groupUsers) + $primaryUsers; |
|
1018 | - } |
|
1019 | - |
|
1020 | - /** |
|
1021 | - * get a list of all groups using a paged search |
|
1022 | - * |
|
1023 | - * @param string $search |
|
1024 | - * @param int $limit |
|
1025 | - * @param int $offset |
|
1026 | - * @return array with group names |
|
1027 | - * |
|
1028 | - * Returns a list with all groups |
|
1029 | - * Uses a paged search if available to override a |
|
1030 | - * server side search limit. |
|
1031 | - * (active directory has a limit of 1000 by default) |
|
1032 | - * @throws Exception |
|
1033 | - */ |
|
1034 | - public function getGroups($search = '', $limit = -1, $offset = 0) { |
|
1035 | - if (!$this->enabled) { |
|
1036 | - return []; |
|
1037 | - } |
|
1038 | - $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset; |
|
1039 | - |
|
1040 | - //Check cache before driving unnecessary searches |
|
1041 | - $ldap_groups = $this->access->connection->getFromCache($cacheKey); |
|
1042 | - if (!is_null($ldap_groups)) { |
|
1043 | - return $ldap_groups; |
|
1044 | - } |
|
1045 | - |
|
1046 | - // if we'd pass -1 to LDAP search, we'd end up in a Protocol |
|
1047 | - // error. With a limit of 0, we get 0 results. So we pass null. |
|
1048 | - if ($limit <= 0) { |
|
1049 | - $limit = null; |
|
1050 | - } |
|
1051 | - $filter = $this->access->combineFilterWithAnd([ |
|
1052 | - $this->access->connection->ldapGroupFilter, |
|
1053 | - $this->access->getFilterPartForGroupSearch($search) |
|
1054 | - ]); |
|
1055 | - $ldap_groups = $this->access->fetchListOfGroups($filter, |
|
1056 | - [$this->access->connection->ldapGroupDisplayName, 'dn'], |
|
1057 | - $limit, |
|
1058 | - $offset); |
|
1059 | - $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups); |
|
1060 | - |
|
1061 | - $this->access->connection->writeToCache($cacheKey, $ldap_groups); |
|
1062 | - return $ldap_groups; |
|
1063 | - } |
|
1064 | - |
|
1065 | - /** |
|
1066 | - * check if a group exists |
|
1067 | - * |
|
1068 | - * @param string $gid |
|
1069 | - * @return bool |
|
1070 | - * @throws ServerNotAvailableException |
|
1071 | - */ |
|
1072 | - public function groupExists($gid) { |
|
1073 | - $groupExists = $this->access->connection->getFromCache('groupExists' . $gid); |
|
1074 | - if (!is_null($groupExists)) { |
|
1075 | - return (bool)$groupExists; |
|
1076 | - } |
|
1077 | - |
|
1078 | - //getting dn, if false the group does not exist. If dn, it may be mapped |
|
1079 | - //only, requires more checking. |
|
1080 | - $dn = $this->access->groupname2dn($gid); |
|
1081 | - if (!$dn) { |
|
1082 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1083 | - return false; |
|
1084 | - } |
|
1085 | - |
|
1086 | - if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) { |
|
1087 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1088 | - return false; |
|
1089 | - } |
|
1090 | - |
|
1091 | - //if group really still exists, we will be able to read its objectClass |
|
1092 | - if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) { |
|
1093 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1094 | - return false; |
|
1095 | - } |
|
1096 | - |
|
1097 | - $this->access->connection->writeToCache('groupExists' . $gid, true); |
|
1098 | - return true; |
|
1099 | - } |
|
1100 | - |
|
1101 | - /** |
|
1102 | - * @throws ServerNotAvailableException |
|
1103 | - * @throws Exception |
|
1104 | - */ |
|
1105 | - protected function filterValidGroups(array $listOfGroups): array { |
|
1106 | - $validGroupDNs = []; |
|
1107 | - foreach ($listOfGroups as $key => $item) { |
|
1108 | - $dn = is_string($item) ? $item : $item['dn'][0]; |
|
1109 | - $gid = $this->access->dn2groupname($dn); |
|
1110 | - if (!$gid) { |
|
1111 | - continue; |
|
1112 | - } |
|
1113 | - if ($this->groupExists($gid)) { |
|
1114 | - $validGroupDNs[$key] = $item; |
|
1115 | - } |
|
1116 | - } |
|
1117 | - return $validGroupDNs; |
|
1118 | - } |
|
1119 | - |
|
1120 | - /** |
|
1121 | - * Check if backend implements actions |
|
1122 | - * |
|
1123 | - * @param int $actions bitwise-or'ed actions |
|
1124 | - * @return boolean |
|
1125 | - * |
|
1126 | - * Returns the supported actions as int to be |
|
1127 | - * compared with GroupInterface::CREATE_GROUP etc. |
|
1128 | - */ |
|
1129 | - public function implementsActions($actions) { |
|
1130 | - return (bool)((GroupInterface::COUNT_USERS | |
|
1131 | - $this->groupPluginManager->getImplementedActions()) & $actions); |
|
1132 | - } |
|
1133 | - |
|
1134 | - /** |
|
1135 | - * Return access for LDAP interaction. |
|
1136 | - * |
|
1137 | - * @return Access instance of Access for LDAP interaction |
|
1138 | - */ |
|
1139 | - public function getLDAPAccess($gid) { |
|
1140 | - return $this->access; |
|
1141 | - } |
|
1142 | - |
|
1143 | - /** |
|
1144 | - * create a group |
|
1145 | - * |
|
1146 | - * @param string $gid |
|
1147 | - * @return bool |
|
1148 | - * @throws Exception |
|
1149 | - * @throws ServerNotAvailableException |
|
1150 | - */ |
|
1151 | - public function createGroup($gid) { |
|
1152 | - if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) { |
|
1153 | - if ($dn = $this->groupPluginManager->createGroup($gid)) { |
|
1154 | - //updates group mapping |
|
1155 | - $uuid = $this->access->getUUID($dn, false); |
|
1156 | - if (is_string($uuid)) { |
|
1157 | - $this->access->mapAndAnnounceIfApplicable( |
|
1158 | - $this->access->getGroupMapper(), |
|
1159 | - $dn, |
|
1160 | - $gid, |
|
1161 | - $uuid, |
|
1162 | - false |
|
1163 | - ); |
|
1164 | - $this->access->cacheGroupExists($gid); |
|
1165 | - } |
|
1166 | - } |
|
1167 | - return $dn != null; |
|
1168 | - } |
|
1169 | - throw new Exception('Could not create group in LDAP backend.'); |
|
1170 | - } |
|
1171 | - |
|
1172 | - /** |
|
1173 | - * delete a group |
|
1174 | - * |
|
1175 | - * @param string $gid gid of the group to delete |
|
1176 | - * @return bool |
|
1177 | - * @throws Exception |
|
1178 | - */ |
|
1179 | - public function deleteGroup($gid) { |
|
1180 | - if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) { |
|
1181 | - if ($ret = $this->groupPluginManager->deleteGroup($gid)) { |
|
1182 | - #delete group in nextcloud internal db |
|
1183 | - $this->access->getGroupMapper()->unmap($gid); |
|
1184 | - $this->access->connection->writeToCache("groupExists" . $gid, false); |
|
1185 | - } |
|
1186 | - return $ret; |
|
1187 | - } |
|
1188 | - throw new Exception('Could not delete group in LDAP backend.'); |
|
1189 | - } |
|
1190 | - |
|
1191 | - /** |
|
1192 | - * Add a user to a group |
|
1193 | - * |
|
1194 | - * @param string $uid Name of the user to add to group |
|
1195 | - * @param string $gid Name of the group in which add the user |
|
1196 | - * @return bool |
|
1197 | - * @throws Exception |
|
1198 | - */ |
|
1199 | - public function addToGroup($uid, $gid) { |
|
1200 | - if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) { |
|
1201 | - if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) { |
|
1202 | - $this->access->connection->clearCache(); |
|
1203 | - unset($this->cachedGroupMembers[$gid]); |
|
1204 | - } |
|
1205 | - return $ret; |
|
1206 | - } |
|
1207 | - throw new Exception('Could not add user to group in LDAP backend.'); |
|
1208 | - } |
|
1209 | - |
|
1210 | - /** |
|
1211 | - * Removes a user from a group |
|
1212 | - * |
|
1213 | - * @param string $uid Name of the user to remove from group |
|
1214 | - * @param string $gid Name of the group from which remove the user |
|
1215 | - * @return bool |
|
1216 | - * @throws Exception |
|
1217 | - */ |
|
1218 | - public function removeFromGroup($uid, $gid) { |
|
1219 | - if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) { |
|
1220 | - if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) { |
|
1221 | - $this->access->connection->clearCache(); |
|
1222 | - unset($this->cachedGroupMembers[$gid]); |
|
1223 | - } |
|
1224 | - return $ret; |
|
1225 | - } |
|
1226 | - throw new Exception('Could not remove user from group in LDAP backend.'); |
|
1227 | - } |
|
1228 | - |
|
1229 | - /** |
|
1230 | - * Gets group details |
|
1231 | - * |
|
1232 | - * @param string $gid Name of the group |
|
1233 | - * @return array|false |
|
1234 | - * @throws Exception |
|
1235 | - */ |
|
1236 | - public function getGroupDetails($gid) { |
|
1237 | - if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) { |
|
1238 | - return $this->groupPluginManager->getGroupDetails($gid); |
|
1239 | - } |
|
1240 | - throw new Exception('Could not get group details in LDAP backend.'); |
|
1241 | - } |
|
1242 | - |
|
1243 | - /** |
|
1244 | - * Return LDAP connection resource from a cloned connection. |
|
1245 | - * The cloned connection needs to be closed manually. |
|
1246 | - * of the current access. |
|
1247 | - * |
|
1248 | - * @param string $gid |
|
1249 | - * @return resource of the LDAP connection |
|
1250 | - * @throws ServerNotAvailableException |
|
1251 | - */ |
|
1252 | - public function getNewLDAPConnection($gid) { |
|
1253 | - $connection = clone $this->access->getConnection(); |
|
1254 | - return $connection->getConnectionResource(); |
|
1255 | - } |
|
1256 | - |
|
1257 | - /** |
|
1258 | - * @throws ServerNotAvailableException |
|
1259 | - */ |
|
1260 | - public function getDisplayName(string $gid): string { |
|
1261 | - if ($this->groupPluginManager instanceof IGetDisplayNameBackend) { |
|
1262 | - return $this->groupPluginManager->getDisplayName($gid); |
|
1263 | - } |
|
1264 | - |
|
1265 | - $cacheKey = 'group_getDisplayName' . $gid; |
|
1266 | - if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) { |
|
1267 | - return $displayName; |
|
1268 | - } |
|
1269 | - |
|
1270 | - $displayName = $this->access->readAttribute( |
|
1271 | - $this->access->groupname2dn($gid), |
|
1272 | - $this->access->connection->ldapGroupDisplayName); |
|
1273 | - |
|
1274 | - if ($displayName && (count($displayName) > 0)) { |
|
1275 | - $displayName = $displayName[0]; |
|
1276 | - $this->access->connection->writeToCache($cacheKey, $displayName); |
|
1277 | - return $displayName; |
|
1278 | - } |
|
1279 | - |
|
1280 | - return ''; |
|
1281 | - } |
|
57 | + protected $enabled = false; |
|
58 | + |
|
59 | + /** @var string[] $cachedGroupMembers array of users with gid as key */ |
|
60 | + protected $cachedGroupMembers; |
|
61 | + /** @var string[] $cachedGroupsByMember array of groups with uid as key */ |
|
62 | + protected $cachedGroupsByMember; |
|
63 | + /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */ |
|
64 | + protected $cachedNestedGroups; |
|
65 | + /** @var GroupPluginManager */ |
|
66 | + protected $groupPluginManager; |
|
67 | + /** @var ILogger */ |
|
68 | + protected $logger; |
|
69 | + |
|
70 | + /** |
|
71 | + * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name |
|
72 | + */ |
|
73 | + protected $ldapGroupMemberAssocAttr; |
|
74 | + |
|
75 | + public function __construct(Access $access, GroupPluginManager $groupPluginManager) { |
|
76 | + parent::__construct($access); |
|
77 | + $filter = $this->access->connection->ldapGroupFilter; |
|
78 | + $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr; |
|
79 | + if (!empty($filter) && !empty($gAssoc)) { |
|
80 | + $this->enabled = true; |
|
81 | + } |
|
82 | + |
|
83 | + $this->cachedGroupMembers = new CappedMemoryCache(); |
|
84 | + $this->cachedGroupsByMember = new CappedMemoryCache(); |
|
85 | + $this->cachedNestedGroups = new CappedMemoryCache(); |
|
86 | + $this->groupPluginManager = $groupPluginManager; |
|
87 | + $this->logger = OC::$server->getLogger(); |
|
88 | + $this->ldapGroupMemberAssocAttr = strtolower($gAssoc); |
|
89 | + } |
|
90 | + |
|
91 | + /** |
|
92 | + * is user in group? |
|
93 | + * |
|
94 | + * @param string $uid uid of the user |
|
95 | + * @param string $gid gid of the group |
|
96 | + * @return bool |
|
97 | + * @throws Exception |
|
98 | + * @throws ServerNotAvailableException |
|
99 | + */ |
|
100 | + public function inGroup($uid, $gid) { |
|
101 | + if (!$this->enabled) { |
|
102 | + return false; |
|
103 | + } |
|
104 | + $cacheKey = 'inGroup' . $uid . ':' . $gid; |
|
105 | + $inGroup = $this->access->connection->getFromCache($cacheKey); |
|
106 | + if (!is_null($inGroup)) { |
|
107 | + return (bool)$inGroup; |
|
108 | + } |
|
109 | + |
|
110 | + $userDN = $this->access->username2dn($uid); |
|
111 | + |
|
112 | + if (isset($this->cachedGroupMembers[$gid])) { |
|
113 | + return in_array($userDN, $this->cachedGroupMembers[$gid]); |
|
114 | + } |
|
115 | + |
|
116 | + $cacheKeyMembers = 'inGroup-members:' . $gid; |
|
117 | + $members = $this->access->connection->getFromCache($cacheKeyMembers); |
|
118 | + if (!is_null($members)) { |
|
119 | + $this->cachedGroupMembers[$gid] = $members; |
|
120 | + $isInGroup = in_array($userDN, $members, true); |
|
121 | + $this->access->connection->writeToCache($cacheKey, $isInGroup); |
|
122 | + return $isInGroup; |
|
123 | + } |
|
124 | + |
|
125 | + $groupDN = $this->access->groupname2dn($gid); |
|
126 | + // just in case |
|
127 | + if (!$groupDN || !$userDN) { |
|
128 | + $this->access->connection->writeToCache($cacheKey, false); |
|
129 | + return false; |
|
130 | + } |
|
131 | + |
|
132 | + //check primary group first |
|
133 | + if ($gid === $this->getUserPrimaryGroup($userDN)) { |
|
134 | + $this->access->connection->writeToCache($cacheKey, true); |
|
135 | + return true; |
|
136 | + } |
|
137 | + |
|
138 | + //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course. |
|
139 | + $members = $this->_groupMembers($groupDN); |
|
140 | + if (!is_array($members) || count($members) === 0) { |
|
141 | + $this->access->connection->writeToCache($cacheKey, false); |
|
142 | + return false; |
|
143 | + } |
|
144 | + |
|
145 | + //extra work if we don't get back user DNs |
|
146 | + switch ($this->ldapGroupMemberAssocAttr) { |
|
147 | + case 'memberuid': |
|
148 | + case 'zimbramailforwardingaddress': |
|
149 | + $requestAttributes = $this->access->userManager->getAttributes(true); |
|
150 | + $dns = []; |
|
151 | + $filterParts = []; |
|
152 | + $bytes = 0; |
|
153 | + foreach ($members as $mid) { |
|
154 | + if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
155 | + $parts = explode('@', $mid); //making sure we get only the uid |
|
156 | + $mid = $parts[0]; |
|
157 | + } |
|
158 | + $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter); |
|
159 | + $filterParts[] = $filter; |
|
160 | + $bytes += strlen($filter); |
|
161 | + if ($bytes >= 9000000) { |
|
162 | + // AD has a default input buffer of 10 MB, we do not want |
|
163 | + // to take even the chance to exceed it |
|
164 | + $filter = $this->access->combineFilterWithOr($filterParts); |
|
165 | + $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts)); |
|
166 | + $bytes = 0; |
|
167 | + $filterParts = []; |
|
168 | + $dns = array_merge($dns, $users); |
|
169 | + } |
|
170 | + } |
|
171 | + if (count($filterParts) > 0) { |
|
172 | + $filter = $this->access->combineFilterWithOr($filterParts); |
|
173 | + $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts)); |
|
174 | + $dns = array_merge($dns, $users); |
|
175 | + } |
|
176 | + $members = $dns; |
|
177 | + break; |
|
178 | + } |
|
179 | + |
|
180 | + $isInGroup = in_array($userDN, $members); |
|
181 | + $this->access->connection->writeToCache($cacheKey, $isInGroup); |
|
182 | + $this->access->connection->writeToCache($cacheKeyMembers, $members); |
|
183 | + $this->cachedGroupMembers[$gid] = $members; |
|
184 | + |
|
185 | + return $isInGroup; |
|
186 | + } |
|
187 | + |
|
188 | + /** |
|
189 | + * For a group that has user membership defined by an LDAP search url |
|
190 | + * attribute returns the users that match the search url otherwise returns |
|
191 | + * an empty array. |
|
192 | + * |
|
193 | + * @throws ServerNotAvailableException |
|
194 | + */ |
|
195 | + public function getDynamicGroupMembers(string $dnGroup): array { |
|
196 | + $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL); |
|
197 | + |
|
198 | + if (empty($dynamicGroupMemberURL)) { |
|
199 | + return []; |
|
200 | + } |
|
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 | + /** |
|
233 | + * @throws ServerNotAvailableException |
|
234 | + */ |
|
235 | + private function _groupMembers(string $dnGroup, ?array &$seen = null): array { |
|
236 | + if ($seen === null) { |
|
237 | + $seen = []; |
|
238 | + } |
|
239 | + $allMembers = []; |
|
240 | + if (array_key_exists($dnGroup, $seen)) { |
|
241 | + return []; |
|
242 | + } |
|
243 | + // used extensively in cron job, caching makes sense for nested groups |
|
244 | + $cacheKey = '_groupMembers' . $dnGroup; |
|
245 | + $groupMembers = $this->access->connection->getFromCache($cacheKey); |
|
246 | + if ($groupMembers !== null) { |
|
247 | + return $groupMembers; |
|
248 | + } |
|
249 | + $seen[$dnGroup] = 1; |
|
250 | + $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr); |
|
251 | + if (is_array($members)) { |
|
252 | + $fetcher = function ($memberDN, &$seen) { |
|
253 | + return $this->_groupMembers($memberDN, $seen); |
|
254 | + }; |
|
255 | + $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members); |
|
256 | + } |
|
257 | + |
|
258 | + $allMembers += $this->getDynamicGroupMembers($dnGroup); |
|
259 | + |
|
260 | + $this->access->connection->writeToCache($cacheKey, $allMembers); |
|
261 | + return $allMembers; |
|
262 | + } |
|
263 | + |
|
264 | + /** |
|
265 | + * @throws ServerNotAvailableException |
|
266 | + */ |
|
267 | + private function _getGroupDNsFromMemberOf(string $dn): array { |
|
268 | + $groups = $this->access->readAttribute($dn, 'memberOf'); |
|
269 | + if (!is_array($groups)) { |
|
270 | + return []; |
|
271 | + } |
|
272 | + |
|
273 | + $fetcher = function ($groupDN) { |
|
274 | + if (isset($this->cachedNestedGroups[$groupDN])) { |
|
275 | + $nestedGroups = $this->cachedNestedGroups[$groupDN]; |
|
276 | + } else { |
|
277 | + $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf'); |
|
278 | + if (!is_array($nestedGroups)) { |
|
279 | + $nestedGroups = []; |
|
280 | + } |
|
281 | + $this->cachedNestedGroups[$groupDN] = $nestedGroups; |
|
282 | + } |
|
283 | + return $nestedGroups; |
|
284 | + }; |
|
285 | + |
|
286 | + $groups = $this->walkNestedGroups($dn, $fetcher, $groups); |
|
287 | + return $this->filterValidGroups($groups); |
|
288 | + } |
|
289 | + |
|
290 | + private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array { |
|
291 | + $nesting = (int)$this->access->connection->ldapNestedGroups; |
|
292 | + // depending on the input, we either have a list of DNs or a list of LDAP records |
|
293 | + // also, the output expects either DNs or records. Testing the first element should suffice. |
|
294 | + $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]); |
|
295 | + |
|
296 | + if ($nesting !== 1) { |
|
297 | + if ($recordMode) { |
|
298 | + // the keys are numeric, but should hold the DN |
|
299 | + return array_reduce($list, function ($transformed, $record) use ($dn) { |
|
300 | + if ($record['dn'][0] != $dn) { |
|
301 | + $transformed[$record['dn'][0]] = $record; |
|
302 | + } |
|
303 | + return $transformed; |
|
304 | + }, []); |
|
305 | + } |
|
306 | + return $list; |
|
307 | + } |
|
308 | + |
|
309 | + $seen = []; |
|
310 | + while ($record = array_pop($list)) { |
|
311 | + $recordDN = $recordMode ? $record['dn'][0] : $record; |
|
312 | + if ($recordDN === $dn || array_key_exists($recordDN, $seen)) { |
|
313 | + // Prevent loops |
|
314 | + continue; |
|
315 | + } |
|
316 | + $fetched = $fetcher($record, $seen); |
|
317 | + $list = array_merge($list, $fetched); |
|
318 | + $seen[$recordDN] = $record; |
|
319 | + } |
|
320 | + |
|
321 | + return $recordMode ? $seen : array_keys($seen); |
|
322 | + } |
|
323 | + |
|
324 | + /** |
|
325 | + * translates a gidNumber into an ownCloud internal name |
|
326 | + * |
|
327 | + * @return string|bool |
|
328 | + * @throws Exception |
|
329 | + * @throws ServerNotAvailableException |
|
330 | + */ |
|
331 | + public function gidNumber2Name(string $gid, string $dn) { |
|
332 | + $cacheKey = 'gidNumberToName' . $gid; |
|
333 | + $groupName = $this->access->connection->getFromCache($cacheKey); |
|
334 | + if (!is_null($groupName) && isset($groupName)) { |
|
335 | + return $groupName; |
|
336 | + } |
|
337 | + |
|
338 | + //we need to get the DN from LDAP |
|
339 | + $filter = $this->access->combineFilterWithAnd([ |
|
340 | + $this->access->connection->ldapGroupFilter, |
|
341 | + 'objectClass=posixGroup', |
|
342 | + $this->access->connection->ldapGidNumber . '=' . $gid |
|
343 | + ]); |
|
344 | + return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
|
345 | + } |
|
346 | + |
|
347 | + /** |
|
348 | + * @throws ServerNotAvailableException |
|
349 | + * @throws Exception |
|
350 | + */ |
|
351 | + private function getNameOfGroup(string $filter, string $cacheKey) { |
|
352 | + $result = $this->access->searchGroups($filter, ['dn'], 1); |
|
353 | + if (empty($result)) { |
|
354 | + return null; |
|
355 | + } |
|
356 | + $dn = $result[0]['dn'][0]; |
|
357 | + |
|
358 | + //and now the group name |
|
359 | + //NOTE once we have separate Nextcloud group IDs and group names we can |
|
360 | + //directly read the display name attribute instead of the DN |
|
361 | + $name = $this->access->dn2groupname($dn); |
|
362 | + |
|
363 | + $this->access->connection->writeToCache($cacheKey, $name); |
|
364 | + |
|
365 | + return $name; |
|
366 | + } |
|
367 | + |
|
368 | + /** |
|
369 | + * returns the entry's gidNumber |
|
370 | + * |
|
371 | + * @return string|bool |
|
372 | + * @throws ServerNotAvailableException |
|
373 | + */ |
|
374 | + private function getEntryGidNumber(string $dn, string $attribute) { |
|
375 | + $value = $this->access->readAttribute($dn, $attribute); |
|
376 | + if (is_array($value) && !empty($value)) { |
|
377 | + return $value[0]; |
|
378 | + } |
|
379 | + return false; |
|
380 | + } |
|
381 | + |
|
382 | + /** |
|
383 | + * @return string|bool |
|
384 | + * @throws ServerNotAvailableException |
|
385 | + */ |
|
386 | + public function getGroupGidNumber(string $dn) { |
|
387 | + return $this->getEntryGidNumber($dn, 'gidNumber'); |
|
388 | + } |
|
389 | + |
|
390 | + /** |
|
391 | + * returns the user's gidNumber |
|
392 | + * |
|
393 | + * @return string|bool |
|
394 | + * @throws ServerNotAvailableException |
|
395 | + */ |
|
396 | + public function getUserGidNumber(string $dn) { |
|
397 | + $gidNumber = false; |
|
398 | + if ($this->access->connection->hasGidNumber) { |
|
399 | + $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber); |
|
400 | + if ($gidNumber === false) { |
|
401 | + $this->access->connection->hasGidNumber = false; |
|
402 | + } |
|
403 | + } |
|
404 | + return $gidNumber; |
|
405 | + } |
|
406 | + |
|
407 | + /** |
|
408 | + * @throws ServerNotAvailableException |
|
409 | + * @throws Exception |
|
410 | + */ |
|
411 | + private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string { |
|
412 | + $groupID = $this->getGroupGidNumber($groupDN); |
|
413 | + if ($groupID === false) { |
|
414 | + throw new Exception('Not a valid group'); |
|
415 | + } |
|
416 | + |
|
417 | + $filterParts = []; |
|
418 | + $filterParts[] = $this->access->getFilterForUserCount(); |
|
419 | + if ($search !== '') { |
|
420 | + $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
|
421 | + } |
|
422 | + $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID; |
|
423 | + |
|
424 | + return $this->access->combineFilterWithAnd($filterParts); |
|
425 | + } |
|
426 | + |
|
427 | + /** |
|
428 | + * returns a list of users that have the given group as gid number |
|
429 | + * |
|
430 | + * @throws ServerNotAvailableException |
|
431 | + */ |
|
432 | + public function getUsersInGidNumber( |
|
433 | + string $groupDN, |
|
434 | + string $search = '', |
|
435 | + ?int $limit = -1, |
|
436 | + ?int $offset = 0 |
|
437 | + ): array { |
|
438 | + try { |
|
439 | + $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search); |
|
440 | + $users = $this->access->fetchListOfUsers( |
|
441 | + $filter, |
|
442 | + [$this->access->connection->ldapUserDisplayName, 'dn'], |
|
443 | + $limit, |
|
444 | + $offset |
|
445 | + ); |
|
446 | + return $this->access->nextcloudUserNames($users); |
|
447 | + } catch (ServerNotAvailableException $e) { |
|
448 | + throw $e; |
|
449 | + } catch (Exception $e) { |
|
450 | + return []; |
|
451 | + } |
|
452 | + } |
|
453 | + |
|
454 | + /** |
|
455 | + * @throws ServerNotAvailableException |
|
456 | + * @return bool |
|
457 | + */ |
|
458 | + public function getUserGroupByGid(string $dn) { |
|
459 | + $groupID = $this->getUserGidNumber($dn); |
|
460 | + if ($groupID !== false) { |
|
461 | + $groupName = $this->gidNumber2Name($groupID, $dn); |
|
462 | + if ($groupName !== false) { |
|
463 | + return $groupName; |
|
464 | + } |
|
465 | + } |
|
466 | + |
|
467 | + return false; |
|
468 | + } |
|
469 | + |
|
470 | + /** |
|
471 | + * translates a primary group ID into an Nextcloud internal name |
|
472 | + * |
|
473 | + * @return string|bool |
|
474 | + * @throws Exception |
|
475 | + * @throws ServerNotAvailableException |
|
476 | + */ |
|
477 | + public function primaryGroupID2Name(string $gid, string $dn) { |
|
478 | + $cacheKey = 'primaryGroupIDtoName'; |
|
479 | + $groupNames = $this->access->connection->getFromCache($cacheKey); |
|
480 | + if (!is_null($groupNames) && isset($groupNames[$gid])) { |
|
481 | + return $groupNames[$gid]; |
|
482 | + } |
|
483 | + |
|
484 | + $domainObjectSid = $this->access->getSID($dn); |
|
485 | + if ($domainObjectSid === false) { |
|
486 | + return false; |
|
487 | + } |
|
488 | + |
|
489 | + //we need to get the DN from LDAP |
|
490 | + $filter = $this->access->combineFilterWithAnd([ |
|
491 | + $this->access->connection->ldapGroupFilter, |
|
492 | + 'objectsid=' . $domainObjectSid . '-' . $gid |
|
493 | + ]); |
|
494 | + return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
|
495 | + } |
|
496 | + |
|
497 | + /** |
|
498 | + * returns the entry's primary group ID |
|
499 | + * |
|
500 | + * @return string|bool |
|
501 | + * @throws ServerNotAvailableException |
|
502 | + */ |
|
503 | + private function getEntryGroupID(string $dn, string $attribute) { |
|
504 | + $value = $this->access->readAttribute($dn, $attribute); |
|
505 | + if (is_array($value) && !empty($value)) { |
|
506 | + return $value[0]; |
|
507 | + } |
|
508 | + return false; |
|
509 | + } |
|
510 | + |
|
511 | + /** |
|
512 | + * @return string|bool |
|
513 | + * @throws ServerNotAvailableException |
|
514 | + */ |
|
515 | + public function getGroupPrimaryGroupID(string $dn) { |
|
516 | + return $this->getEntryGroupID($dn, 'primaryGroupToken'); |
|
517 | + } |
|
518 | + |
|
519 | + /** |
|
520 | + * @return string|bool |
|
521 | + * @throws ServerNotAvailableException |
|
522 | + */ |
|
523 | + public function getUserPrimaryGroupIDs(string $dn) { |
|
524 | + $primaryGroupID = false; |
|
525 | + if ($this->access->connection->hasPrimaryGroups) { |
|
526 | + $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID'); |
|
527 | + if ($primaryGroupID === false) { |
|
528 | + $this->access->connection->hasPrimaryGroups = false; |
|
529 | + } |
|
530 | + } |
|
531 | + return $primaryGroupID; |
|
532 | + } |
|
533 | + |
|
534 | + /** |
|
535 | + * @throws Exception |
|
536 | + * @throws ServerNotAvailableException |
|
537 | + */ |
|
538 | + private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string { |
|
539 | + $groupID = $this->getGroupPrimaryGroupID($groupDN); |
|
540 | + if ($groupID === false) { |
|
541 | + throw new Exception('Not a valid group'); |
|
542 | + } |
|
543 | + |
|
544 | + $filterParts = []; |
|
545 | + $filterParts[] = $this->access->getFilterForUserCount(); |
|
546 | + if ($search !== '') { |
|
547 | + $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
|
548 | + } |
|
549 | + $filterParts[] = 'primaryGroupID=' . $groupID; |
|
550 | + |
|
551 | + return $this->access->combineFilterWithAnd($filterParts); |
|
552 | + } |
|
553 | + |
|
554 | + /** |
|
555 | + * @throws ServerNotAvailableException |
|
556 | + */ |
|
557 | + public function getUsersInPrimaryGroup( |
|
558 | + string $groupDN, |
|
559 | + string $search = '', |
|
560 | + ?int $limit = -1, |
|
561 | + ?int $offset = 0 |
|
562 | + ): array { |
|
563 | + try { |
|
564 | + $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); |
|
565 | + $users = $this->access->fetchListOfUsers( |
|
566 | + $filter, |
|
567 | + [$this->access->connection->ldapUserDisplayName, 'dn'], |
|
568 | + $limit, |
|
569 | + $offset |
|
570 | + ); |
|
571 | + return $this->access->nextcloudUserNames($users); |
|
572 | + } catch (ServerNotAvailableException $e) { |
|
573 | + throw $e; |
|
574 | + } catch (Exception $e) { |
|
575 | + return []; |
|
576 | + } |
|
577 | + } |
|
578 | + |
|
579 | + /** |
|
580 | + * @throws ServerNotAvailableException |
|
581 | + */ |
|
582 | + public function countUsersInPrimaryGroup( |
|
583 | + string $groupDN, |
|
584 | + string $search = '', |
|
585 | + int $limit = -1, |
|
586 | + int $offset = 0 |
|
587 | + ): int { |
|
588 | + try { |
|
589 | + $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); |
|
590 | + $users = $this->access->countUsers($filter, ['dn'], $limit, $offset); |
|
591 | + return (int)$users; |
|
592 | + } catch (ServerNotAvailableException $e) { |
|
593 | + throw $e; |
|
594 | + } catch (Exception $e) { |
|
595 | + return 0; |
|
596 | + } |
|
597 | + } |
|
598 | + |
|
599 | + /** |
|
600 | + * @return string|bool |
|
601 | + * @throws ServerNotAvailableException |
|
602 | + */ |
|
603 | + public function getUserPrimaryGroup(string $dn) { |
|
604 | + $groupID = $this->getUserPrimaryGroupIDs($dn); |
|
605 | + if ($groupID !== false) { |
|
606 | + $groupName = $this->primaryGroupID2Name($groupID, $dn); |
|
607 | + if ($groupName !== false) { |
|
608 | + return $groupName; |
|
609 | + } |
|
610 | + } |
|
611 | + |
|
612 | + return false; |
|
613 | + } |
|
614 | + |
|
615 | + /** |
|
616 | + * This function fetches all groups a user belongs to. It does not check |
|
617 | + * if the user exists at all. |
|
618 | + * |
|
619 | + * This function includes groups based on dynamic group membership. |
|
620 | + * |
|
621 | + * @param string $uid Name of the user |
|
622 | + * @return array with group names |
|
623 | + * @throws Exception |
|
624 | + * @throws ServerNotAvailableException |
|
625 | + */ |
|
626 | + public function getUserGroups($uid) { |
|
627 | + if (!$this->enabled) { |
|
628 | + return []; |
|
629 | + } |
|
630 | + $cacheKey = 'getUserGroups' . $uid; |
|
631 | + $userGroups = $this->access->connection->getFromCache($cacheKey); |
|
632 | + if (!is_null($userGroups)) { |
|
633 | + return $userGroups; |
|
634 | + } |
|
635 | + $userDN = $this->access->username2dn($uid); |
|
636 | + if (!$userDN) { |
|
637 | + $this->access->connection->writeToCache($cacheKey, []); |
|
638 | + return []; |
|
639 | + } |
|
640 | + |
|
641 | + $groups = []; |
|
642 | + $primaryGroup = $this->getUserPrimaryGroup($userDN); |
|
643 | + $gidGroupName = $this->getUserGroupByGid($userDN); |
|
644 | + |
|
645 | + $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL); |
|
646 | + |
|
647 | + if (!empty($dynamicGroupMemberURL)) { |
|
648 | + // look through dynamic groups to add them to the result array if needed |
|
649 | + $groupsToMatch = $this->access->fetchListOfGroups( |
|
650 | + $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]); |
|
651 | + foreach ($groupsToMatch as $dynamicGroup) { |
|
652 | + if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) { |
|
653 | + continue; |
|
654 | + } |
|
655 | + $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '('); |
|
656 | + if ($pos !== false) { |
|
657 | + $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos); |
|
658 | + // apply filter via ldap search to see if this user is in this |
|
659 | + // dynamic group |
|
660 | + $userMatch = $this->access->readAttribute( |
|
661 | + $userDN, |
|
662 | + $this->access->connection->ldapUserDisplayName, |
|
663 | + $memberUrlFilter |
|
664 | + ); |
|
665 | + if ($userMatch !== false) { |
|
666 | + // match found so this user is in this group |
|
667 | + $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]); |
|
668 | + if (is_string($groupName)) { |
|
669 | + // be sure to never return false if the dn could not be |
|
670 | + // resolved to a name, for whatever reason. |
|
671 | + $groups[] = $groupName; |
|
672 | + } |
|
673 | + } |
|
674 | + } else { |
|
675 | + $this->logger->debug('No search filter found on member url of group {dn}', |
|
676 | + [ |
|
677 | + 'app' => 'user_ldap', |
|
678 | + 'dn' => $dynamicGroup, |
|
679 | + ] |
|
680 | + ); |
|
681 | + } |
|
682 | + } |
|
683 | + } |
|
684 | + |
|
685 | + // if possible, read out membership via memberOf. It's far faster than |
|
686 | + // performing a search, which still is a fallback later. |
|
687 | + // memberof doesn't support memberuid, so skip it here. |
|
688 | + if ((int)$this->access->connection->hasMemberOfFilterSupport === 1 |
|
689 | + && (int)$this->access->connection->useMemberOfToDetectMembership === 1 |
|
690 | + && $this->ldapGroupMemberAssocAttr !== 'memberuid' |
|
691 | + && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') { |
|
692 | + $groupDNs = $this->_getGroupDNsFromMemberOf($userDN); |
|
693 | + if (is_array($groupDNs)) { |
|
694 | + foreach ($groupDNs as $dn) { |
|
695 | + $groupName = $this->access->dn2groupname($dn); |
|
696 | + if (is_string($groupName)) { |
|
697 | + // be sure to never return false if the dn could not be |
|
698 | + // resolved to a name, for whatever reason. |
|
699 | + $groups[] = $groupName; |
|
700 | + } |
|
701 | + } |
|
702 | + } |
|
703 | + |
|
704 | + if ($primaryGroup !== false) { |
|
705 | + $groups[] = $primaryGroup; |
|
706 | + } |
|
707 | + if ($gidGroupName !== false) { |
|
708 | + $groups[] = $gidGroupName; |
|
709 | + } |
|
710 | + $this->access->connection->writeToCache($cacheKey, $groups); |
|
711 | + return $groups; |
|
712 | + } |
|
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 | + |
|
721 | + case 'memberuid': |
|
722 | + case 'zimbramailforwardingaddress': |
|
723 | + $result = $this->access->readAttribute($userDN, 'uid'); |
|
724 | + if ($result === false) { |
|
725 | + $this->logger->debug('No uid attribute found for DN {dn} on {host}', |
|
726 | + [ |
|
727 | + 'app' => 'user_ldap', |
|
728 | + 'dn' => $userDN, |
|
729 | + 'host' => $this->access->connection->ldapHost, |
|
730 | + ] |
|
731 | + ); |
|
732 | + $uid = false; |
|
733 | + } else { |
|
734 | + $uid = $result[0]; |
|
735 | + } |
|
736 | + break; |
|
737 | + |
|
738 | + default: |
|
739 | + // just in case |
|
740 | + $uid = $userDN; |
|
741 | + break; |
|
742 | + } |
|
743 | + |
|
744 | + if ($uid !== false) { |
|
745 | + if (isset($this->cachedGroupsByMember[$uid])) { |
|
746 | + $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]); |
|
747 | + } else { |
|
748 | + $groupsByMember = array_values($this->getGroupsByMember($uid)); |
|
749 | + $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember); |
|
750 | + $this->cachedGroupsByMember[$uid] = $groupsByMember; |
|
751 | + $groups = array_merge($groups, $groupsByMember); |
|
752 | + } |
|
753 | + } |
|
754 | + |
|
755 | + if ($primaryGroup !== false) { |
|
756 | + $groups[] = $primaryGroup; |
|
757 | + } |
|
758 | + if ($gidGroupName !== false) { |
|
759 | + $groups[] = $gidGroupName; |
|
760 | + } |
|
761 | + |
|
762 | + $groups = array_unique($groups, SORT_LOCALE_STRING); |
|
763 | + $this->access->connection->writeToCache($cacheKey, $groups); |
|
764 | + |
|
765 | + return $groups; |
|
766 | + } |
|
767 | + |
|
768 | + /** |
|
769 | + * @throws ServerNotAvailableException |
|
770 | + */ |
|
771 | + private function getGroupsByMember(string $dn, array &$seen = null): array { |
|
772 | + if ($seen === null) { |
|
773 | + $seen = []; |
|
774 | + } |
|
775 | + if (array_key_exists($dn, $seen)) { |
|
776 | + // avoid loops |
|
777 | + return []; |
|
778 | + } |
|
779 | + $allGroups = []; |
|
780 | + $seen[$dn] = true; |
|
781 | + $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn; |
|
782 | + |
|
783 | + if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
784 | + //in this case the member entries are email addresses |
|
785 | + $filter .= '@*'; |
|
786 | + } |
|
787 | + |
|
788 | + $groups = $this->access->fetchListOfGroups($filter, |
|
789 | + [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']); |
|
790 | + if (is_array($groups)) { |
|
791 | + $fetcher = function ($dn, &$seen) { |
|
792 | + if (is_array($dn) && isset($dn['dn'][0])) { |
|
793 | + $dn = $dn['dn'][0]; |
|
794 | + } |
|
795 | + return $this->getGroupsByMember($dn, $seen); |
|
796 | + }; |
|
797 | + |
|
798 | + if (empty($dn)) { |
|
799 | + $dn = ""; |
|
800 | + } |
|
801 | + |
|
802 | + $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups); |
|
803 | + } |
|
804 | + $visibleGroups = $this->filterValidGroups($allGroups); |
|
805 | + return array_intersect_key($allGroups, $visibleGroups); |
|
806 | + } |
|
807 | + |
|
808 | + /** |
|
809 | + * get a list of all users in a group |
|
810 | + * |
|
811 | + * @param string $gid |
|
812 | + * @param string $search |
|
813 | + * @param int $limit |
|
814 | + * @param int $offset |
|
815 | + * @return array with user ids |
|
816 | + * @throws Exception |
|
817 | + * @throws ServerNotAvailableException |
|
818 | + */ |
|
819 | + public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { |
|
820 | + if (!$this->enabled) { |
|
821 | + return []; |
|
822 | + } |
|
823 | + if (!$this->groupExists($gid)) { |
|
824 | + return []; |
|
825 | + } |
|
826 | + $search = $this->access->escapeFilterPart($search, true); |
|
827 | + $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset; |
|
828 | + // check for cache of the exact query |
|
829 | + $groupUsers = $this->access->connection->getFromCache($cacheKey); |
|
830 | + if (!is_null($groupUsers)) { |
|
831 | + return $groupUsers; |
|
832 | + } |
|
833 | + |
|
834 | + if ($limit === -1) { |
|
835 | + $limit = null; |
|
836 | + } |
|
837 | + // check for cache of the query without limit and offset |
|
838 | + $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search); |
|
839 | + if (!is_null($groupUsers)) { |
|
840 | + $groupUsers = array_slice($groupUsers, $offset, $limit); |
|
841 | + $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
842 | + return $groupUsers; |
|
843 | + } |
|
844 | + |
|
845 | + $groupDN = $this->access->groupname2dn($gid); |
|
846 | + if (!$groupDN) { |
|
847 | + // group couldn't be found, return empty resultset |
|
848 | + $this->access->connection->writeToCache($cacheKey, []); |
|
849 | + return []; |
|
850 | + } |
|
851 | + |
|
852 | + $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset); |
|
853 | + $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset); |
|
854 | + $members = $this->_groupMembers($groupDN); |
|
855 | + if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) { |
|
856 | + //in case users could not be retrieved, return empty result set |
|
857 | + $this->access->connection->writeToCache($cacheKey, []); |
|
858 | + return []; |
|
859 | + } |
|
860 | + |
|
861 | + $groupUsers = []; |
|
862 | + $attrs = $this->access->userManager->getAttributes(true); |
|
863 | + foreach ($members as $member) { |
|
864 | + switch ($this->ldapGroupMemberAssocAttr) { |
|
865 | + case 'zimbramailforwardingaddress': |
|
866 | + //we get email addresses and need to convert them to uids |
|
867 | + $parts = explode('@', $member); |
|
868 | + $member = $parts[0]; |
|
869 | + //no break needed because we just needed to remove the email part and now we have uids |
|
870 | + case 'memberuid': |
|
871 | + //we got uids, need to get their DNs to 'translate' them to user names |
|
872 | + $filter = $this->access->combineFilterWithAnd([ |
|
873 | + str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter), |
|
874 | + $this->access->combineFilterWithAnd([ |
|
875 | + $this->access->getFilterPartForUserSearch($search), |
|
876 | + $this->access->connection->ldapUserFilter |
|
877 | + ]) |
|
878 | + ]); |
|
879 | + $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1); |
|
880 | + if (count($ldap_users) < 1) { |
|
881 | + continue; |
|
882 | + } |
|
883 | + $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]); |
|
884 | + break; |
|
885 | + default: |
|
886 | + //we got DNs, check if we need to filter by search or we can give back all of them |
|
887 | + $uid = $this->access->dn2username($member); |
|
888 | + if (!$uid) { |
|
889 | + continue; |
|
890 | + } |
|
891 | + |
|
892 | + $cacheKey = 'userExistsOnLDAP' . $uid; |
|
893 | + $userExists = $this->access->connection->getFromCache($cacheKey); |
|
894 | + if ($userExists === false) { |
|
895 | + continue; |
|
896 | + } |
|
897 | + if ($userExists === null || $search !== '') { |
|
898 | + if (!$this->access->readAttribute($member, |
|
899 | + $this->access->connection->ldapUserDisplayName, |
|
900 | + $this->access->combineFilterWithAnd([ |
|
901 | + $this->access->getFilterPartForUserSearch($search), |
|
902 | + $this->access->connection->ldapUserFilter |
|
903 | + ]))) { |
|
904 | + if ($search === '') { |
|
905 | + $this->access->connection->writeToCache($cacheKey, false); |
|
906 | + } |
|
907 | + continue; |
|
908 | + } |
|
909 | + $this->access->connection->writeToCache($cacheKey, true); |
|
910 | + } |
|
911 | + $groupUsers[] = $uid; |
|
912 | + break; |
|
913 | + } |
|
914 | + } |
|
915 | + |
|
916 | + $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers)); |
|
917 | + natsort($groupUsers); |
|
918 | + $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers); |
|
919 | + $groupUsers = array_slice($groupUsers, $offset, $limit); |
|
920 | + |
|
921 | + $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
922 | + |
|
923 | + return $groupUsers; |
|
924 | + } |
|
925 | + |
|
926 | + /** |
|
927 | + * returns the number of users in a group, who match the search term |
|
928 | + * |
|
929 | + * @param string $gid the internal group name |
|
930 | + * @param string $search optional, a search string |
|
931 | + * @return int|bool |
|
932 | + * @throws Exception |
|
933 | + * @throws ServerNotAvailableException |
|
934 | + */ |
|
935 | + public function countUsersInGroup($gid, $search = '') { |
|
936 | + if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) { |
|
937 | + return $this->groupPluginManager->countUsersInGroup($gid, $search); |
|
938 | + } |
|
939 | + |
|
940 | + $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search; |
|
941 | + if (!$this->enabled || !$this->groupExists($gid)) { |
|
942 | + return false; |
|
943 | + } |
|
944 | + $groupUsers = $this->access->connection->getFromCache($cacheKey); |
|
945 | + if (!is_null($groupUsers)) { |
|
946 | + return $groupUsers; |
|
947 | + } |
|
948 | + |
|
949 | + $groupDN = $this->access->groupname2dn($gid); |
|
950 | + if (!$groupDN) { |
|
951 | + // group couldn't be found, return empty result set |
|
952 | + $this->access->connection->writeToCache($cacheKey, false); |
|
953 | + return false; |
|
954 | + } |
|
955 | + |
|
956 | + $members = $this->_groupMembers($groupDN); |
|
957 | + $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, ''); |
|
958 | + if (!$members && $primaryUserCount === 0) { |
|
959 | + //in case users could not be retrieved, return empty result set |
|
960 | + $this->access->connection->writeToCache($cacheKey, false); |
|
961 | + return false; |
|
962 | + } |
|
963 | + |
|
964 | + if ($search === '') { |
|
965 | + $groupUsers = count($members) + $primaryUserCount; |
|
966 | + $this->access->connection->writeToCache($cacheKey, $groupUsers); |
|
967 | + return $groupUsers; |
|
968 | + } |
|
969 | + $search = $this->access->escapeFilterPart($search, true); |
|
970 | + $isMemberUid = |
|
971 | + ($this->ldapGroupMemberAssocAttr === 'memberuid' || |
|
972 | + $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress'); |
|
973 | + |
|
974 | + //we need to apply the search filter |
|
975 | + //alternatives that need to be checked: |
|
976 | + //a) get all users by search filter and array_intersect them |
|
977 | + //b) a, but only when less than 1k 10k ?k users like it is |
|
978 | + //c) put all DNs|uids in a LDAP filter, combine with the search string |
|
979 | + // and let it count. |
|
980 | + //For now this is not important, because the only use of this method |
|
981 | + //does not supply a search string |
|
982 | + $groupUsers = []; |
|
983 | + foreach ($members as $member) { |
|
984 | + if ($isMemberUid) { |
|
985 | + if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
|
986 | + //we get email addresses and need to convert them to uids |
|
987 | + $parts = explode('@', $member); |
|
988 | + $member = $parts[0]; |
|
989 | + } |
|
990 | + //we got uids, need to get their DNs to 'translate' them to user names |
|
991 | + $filter = $this->access->combineFilterWithAnd([ |
|
992 | + str_replace('%uid', $member, $this->access->connection->ldapLoginFilter), |
|
993 | + $this->access->getFilterPartForUserSearch($search) |
|
994 | + ]); |
|
995 | + $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1); |
|
996 | + if (count($ldap_users) < 1) { |
|
997 | + continue; |
|
998 | + } |
|
999 | + $groupUsers[] = $this->access->dn2username($ldap_users[0]); |
|
1000 | + } else { |
|
1001 | + //we need to apply the search filter now |
|
1002 | + if (!$this->access->readAttribute($member, |
|
1003 | + $this->access->connection->ldapUserDisplayName, |
|
1004 | + $this->access->getFilterPartForUserSearch($search))) { |
|
1005 | + continue; |
|
1006 | + } |
|
1007 | + // dn2username will also check if the users belong to the allowed base |
|
1008 | + if ($ncGroupId = $this->access->dn2username($member)) { |
|
1009 | + $groupUsers[] = $ncGroupId; |
|
1010 | + } |
|
1011 | + } |
|
1012 | + } |
|
1013 | + |
|
1014 | + //and get users that have the group as primary |
|
1015 | + $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search); |
|
1016 | + |
|
1017 | + return count($groupUsers) + $primaryUsers; |
|
1018 | + } |
|
1019 | + |
|
1020 | + /** |
|
1021 | + * get a list of all groups using a paged search |
|
1022 | + * |
|
1023 | + * @param string $search |
|
1024 | + * @param int $limit |
|
1025 | + * @param int $offset |
|
1026 | + * @return array with group names |
|
1027 | + * |
|
1028 | + * Returns a list with all groups |
|
1029 | + * Uses a paged search if available to override a |
|
1030 | + * server side search limit. |
|
1031 | + * (active directory has a limit of 1000 by default) |
|
1032 | + * @throws Exception |
|
1033 | + */ |
|
1034 | + public function getGroups($search = '', $limit = -1, $offset = 0) { |
|
1035 | + if (!$this->enabled) { |
|
1036 | + return []; |
|
1037 | + } |
|
1038 | + $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset; |
|
1039 | + |
|
1040 | + //Check cache before driving unnecessary searches |
|
1041 | + $ldap_groups = $this->access->connection->getFromCache($cacheKey); |
|
1042 | + if (!is_null($ldap_groups)) { |
|
1043 | + return $ldap_groups; |
|
1044 | + } |
|
1045 | + |
|
1046 | + // if we'd pass -1 to LDAP search, we'd end up in a Protocol |
|
1047 | + // error. With a limit of 0, we get 0 results. So we pass null. |
|
1048 | + if ($limit <= 0) { |
|
1049 | + $limit = null; |
|
1050 | + } |
|
1051 | + $filter = $this->access->combineFilterWithAnd([ |
|
1052 | + $this->access->connection->ldapGroupFilter, |
|
1053 | + $this->access->getFilterPartForGroupSearch($search) |
|
1054 | + ]); |
|
1055 | + $ldap_groups = $this->access->fetchListOfGroups($filter, |
|
1056 | + [$this->access->connection->ldapGroupDisplayName, 'dn'], |
|
1057 | + $limit, |
|
1058 | + $offset); |
|
1059 | + $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups); |
|
1060 | + |
|
1061 | + $this->access->connection->writeToCache($cacheKey, $ldap_groups); |
|
1062 | + return $ldap_groups; |
|
1063 | + } |
|
1064 | + |
|
1065 | + /** |
|
1066 | + * check if a group exists |
|
1067 | + * |
|
1068 | + * @param string $gid |
|
1069 | + * @return bool |
|
1070 | + * @throws ServerNotAvailableException |
|
1071 | + */ |
|
1072 | + public function groupExists($gid) { |
|
1073 | + $groupExists = $this->access->connection->getFromCache('groupExists' . $gid); |
|
1074 | + if (!is_null($groupExists)) { |
|
1075 | + return (bool)$groupExists; |
|
1076 | + } |
|
1077 | + |
|
1078 | + //getting dn, if false the group does not exist. If dn, it may be mapped |
|
1079 | + //only, requires more checking. |
|
1080 | + $dn = $this->access->groupname2dn($gid); |
|
1081 | + if (!$dn) { |
|
1082 | + $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1083 | + return false; |
|
1084 | + } |
|
1085 | + |
|
1086 | + if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) { |
|
1087 | + $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1088 | + return false; |
|
1089 | + } |
|
1090 | + |
|
1091 | + //if group really still exists, we will be able to read its objectClass |
|
1092 | + if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) { |
|
1093 | + $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1094 | + return false; |
|
1095 | + } |
|
1096 | + |
|
1097 | + $this->access->connection->writeToCache('groupExists' . $gid, true); |
|
1098 | + return true; |
|
1099 | + } |
|
1100 | + |
|
1101 | + /** |
|
1102 | + * @throws ServerNotAvailableException |
|
1103 | + * @throws Exception |
|
1104 | + */ |
|
1105 | + protected function filterValidGroups(array $listOfGroups): array { |
|
1106 | + $validGroupDNs = []; |
|
1107 | + foreach ($listOfGroups as $key => $item) { |
|
1108 | + $dn = is_string($item) ? $item : $item['dn'][0]; |
|
1109 | + $gid = $this->access->dn2groupname($dn); |
|
1110 | + if (!$gid) { |
|
1111 | + continue; |
|
1112 | + } |
|
1113 | + if ($this->groupExists($gid)) { |
|
1114 | + $validGroupDNs[$key] = $item; |
|
1115 | + } |
|
1116 | + } |
|
1117 | + return $validGroupDNs; |
|
1118 | + } |
|
1119 | + |
|
1120 | + /** |
|
1121 | + * Check if backend implements actions |
|
1122 | + * |
|
1123 | + * @param int $actions bitwise-or'ed actions |
|
1124 | + * @return boolean |
|
1125 | + * |
|
1126 | + * Returns the supported actions as int to be |
|
1127 | + * compared with GroupInterface::CREATE_GROUP etc. |
|
1128 | + */ |
|
1129 | + public function implementsActions($actions) { |
|
1130 | + return (bool)((GroupInterface::COUNT_USERS | |
|
1131 | + $this->groupPluginManager->getImplementedActions()) & $actions); |
|
1132 | + } |
|
1133 | + |
|
1134 | + /** |
|
1135 | + * Return access for LDAP interaction. |
|
1136 | + * |
|
1137 | + * @return Access instance of Access for LDAP interaction |
|
1138 | + */ |
|
1139 | + public function getLDAPAccess($gid) { |
|
1140 | + return $this->access; |
|
1141 | + } |
|
1142 | + |
|
1143 | + /** |
|
1144 | + * create a group |
|
1145 | + * |
|
1146 | + * @param string $gid |
|
1147 | + * @return bool |
|
1148 | + * @throws Exception |
|
1149 | + * @throws ServerNotAvailableException |
|
1150 | + */ |
|
1151 | + public function createGroup($gid) { |
|
1152 | + if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) { |
|
1153 | + if ($dn = $this->groupPluginManager->createGroup($gid)) { |
|
1154 | + //updates group mapping |
|
1155 | + $uuid = $this->access->getUUID($dn, false); |
|
1156 | + if (is_string($uuid)) { |
|
1157 | + $this->access->mapAndAnnounceIfApplicable( |
|
1158 | + $this->access->getGroupMapper(), |
|
1159 | + $dn, |
|
1160 | + $gid, |
|
1161 | + $uuid, |
|
1162 | + false |
|
1163 | + ); |
|
1164 | + $this->access->cacheGroupExists($gid); |
|
1165 | + } |
|
1166 | + } |
|
1167 | + return $dn != null; |
|
1168 | + } |
|
1169 | + throw new Exception('Could not create group in LDAP backend.'); |
|
1170 | + } |
|
1171 | + |
|
1172 | + /** |
|
1173 | + * delete a group |
|
1174 | + * |
|
1175 | + * @param string $gid gid of the group to delete |
|
1176 | + * @return bool |
|
1177 | + * @throws Exception |
|
1178 | + */ |
|
1179 | + public function deleteGroup($gid) { |
|
1180 | + if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) { |
|
1181 | + if ($ret = $this->groupPluginManager->deleteGroup($gid)) { |
|
1182 | + #delete group in nextcloud internal db |
|
1183 | + $this->access->getGroupMapper()->unmap($gid); |
|
1184 | + $this->access->connection->writeToCache("groupExists" . $gid, false); |
|
1185 | + } |
|
1186 | + return $ret; |
|
1187 | + } |
|
1188 | + throw new Exception('Could not delete group in LDAP backend.'); |
|
1189 | + } |
|
1190 | + |
|
1191 | + /** |
|
1192 | + * Add a user to a group |
|
1193 | + * |
|
1194 | + * @param string $uid Name of the user to add to group |
|
1195 | + * @param string $gid Name of the group in which add the user |
|
1196 | + * @return bool |
|
1197 | + * @throws Exception |
|
1198 | + */ |
|
1199 | + public function addToGroup($uid, $gid) { |
|
1200 | + if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) { |
|
1201 | + if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) { |
|
1202 | + $this->access->connection->clearCache(); |
|
1203 | + unset($this->cachedGroupMembers[$gid]); |
|
1204 | + } |
|
1205 | + return $ret; |
|
1206 | + } |
|
1207 | + throw new Exception('Could not add user to group in LDAP backend.'); |
|
1208 | + } |
|
1209 | + |
|
1210 | + /** |
|
1211 | + * Removes a user from a group |
|
1212 | + * |
|
1213 | + * @param string $uid Name of the user to remove from group |
|
1214 | + * @param string $gid Name of the group from which remove the user |
|
1215 | + * @return bool |
|
1216 | + * @throws Exception |
|
1217 | + */ |
|
1218 | + public function removeFromGroup($uid, $gid) { |
|
1219 | + if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) { |
|
1220 | + if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) { |
|
1221 | + $this->access->connection->clearCache(); |
|
1222 | + unset($this->cachedGroupMembers[$gid]); |
|
1223 | + } |
|
1224 | + return $ret; |
|
1225 | + } |
|
1226 | + throw new Exception('Could not remove user from group in LDAP backend.'); |
|
1227 | + } |
|
1228 | + |
|
1229 | + /** |
|
1230 | + * Gets group details |
|
1231 | + * |
|
1232 | + * @param string $gid Name of the group |
|
1233 | + * @return array|false |
|
1234 | + * @throws Exception |
|
1235 | + */ |
|
1236 | + public function getGroupDetails($gid) { |
|
1237 | + if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) { |
|
1238 | + return $this->groupPluginManager->getGroupDetails($gid); |
|
1239 | + } |
|
1240 | + throw new Exception('Could not get group details in LDAP backend.'); |
|
1241 | + } |
|
1242 | + |
|
1243 | + /** |
|
1244 | + * Return LDAP connection resource from a cloned connection. |
|
1245 | + * The cloned connection needs to be closed manually. |
|
1246 | + * of the current access. |
|
1247 | + * |
|
1248 | + * @param string $gid |
|
1249 | + * @return resource of the LDAP connection |
|
1250 | + * @throws ServerNotAvailableException |
|
1251 | + */ |
|
1252 | + public function getNewLDAPConnection($gid) { |
|
1253 | + $connection = clone $this->access->getConnection(); |
|
1254 | + return $connection->getConnectionResource(); |
|
1255 | + } |
|
1256 | + |
|
1257 | + /** |
|
1258 | + * @throws ServerNotAvailableException |
|
1259 | + */ |
|
1260 | + public function getDisplayName(string $gid): string { |
|
1261 | + if ($this->groupPluginManager instanceof IGetDisplayNameBackend) { |
|
1262 | + return $this->groupPluginManager->getDisplayName($gid); |
|
1263 | + } |
|
1264 | + |
|
1265 | + $cacheKey = 'group_getDisplayName' . $gid; |
|
1266 | + if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) { |
|
1267 | + return $displayName; |
|
1268 | + } |
|
1269 | + |
|
1270 | + $displayName = $this->access->readAttribute( |
|
1271 | + $this->access->groupname2dn($gid), |
|
1272 | + $this->access->connection->ldapGroupDisplayName); |
|
1273 | + |
|
1274 | + if ($displayName && (count($displayName) > 0)) { |
|
1275 | + $displayName = $displayName[0]; |
|
1276 | + $this->access->connection->writeToCache($cacheKey, $displayName); |
|
1277 | + return $displayName; |
|
1278 | + } |
|
1279 | + |
|
1280 | + return ''; |
|
1281 | + } |
|
1282 | 1282 | } |
@@ -101,10 +101,10 @@ discard block |
||
101 | 101 | if (!$this->enabled) { |
102 | 102 | return false; |
103 | 103 | } |
104 | - $cacheKey = 'inGroup' . $uid . ':' . $gid; |
|
104 | + $cacheKey = 'inGroup'.$uid.':'.$gid; |
|
105 | 105 | $inGroup = $this->access->connection->getFromCache($cacheKey); |
106 | 106 | if (!is_null($inGroup)) { |
107 | - return (bool)$inGroup; |
|
107 | + return (bool) $inGroup; |
|
108 | 108 | } |
109 | 109 | |
110 | 110 | $userDN = $this->access->username2dn($uid); |
@@ -113,7 +113,7 @@ discard block |
||
113 | 113 | return in_array($userDN, $this->cachedGroupMembers[$gid]); |
114 | 114 | } |
115 | 115 | |
116 | - $cacheKeyMembers = 'inGroup-members:' . $gid; |
|
116 | + $cacheKeyMembers = 'inGroup-members:'.$gid; |
|
117 | 117 | $members = $this->access->connection->getFromCache($cacheKeyMembers); |
118 | 118 | if (!is_null($members)) { |
119 | 119 | $this->cachedGroupMembers[$gid] = $members; |
@@ -241,7 +241,7 @@ discard block |
||
241 | 241 | return []; |
242 | 242 | } |
243 | 243 | // used extensively in cron job, caching makes sense for nested groups |
244 | - $cacheKey = '_groupMembers' . $dnGroup; |
|
244 | + $cacheKey = '_groupMembers'.$dnGroup; |
|
245 | 245 | $groupMembers = $this->access->connection->getFromCache($cacheKey); |
246 | 246 | if ($groupMembers !== null) { |
247 | 247 | return $groupMembers; |
@@ -249,7 +249,7 @@ discard block |
||
249 | 249 | $seen[$dnGroup] = 1; |
250 | 250 | $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr); |
251 | 251 | if (is_array($members)) { |
252 | - $fetcher = function ($memberDN, &$seen) { |
|
252 | + $fetcher = function($memberDN, &$seen) { |
|
253 | 253 | return $this->_groupMembers($memberDN, $seen); |
254 | 254 | }; |
255 | 255 | $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members); |
@@ -270,7 +270,7 @@ discard block |
||
270 | 270 | return []; |
271 | 271 | } |
272 | 272 | |
273 | - $fetcher = function ($groupDN) { |
|
273 | + $fetcher = function($groupDN) { |
|
274 | 274 | if (isset($this->cachedNestedGroups[$groupDN])) { |
275 | 275 | $nestedGroups = $this->cachedNestedGroups[$groupDN]; |
276 | 276 | } else { |
@@ -288,7 +288,7 @@ discard block |
||
288 | 288 | } |
289 | 289 | |
290 | 290 | private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array { |
291 | - $nesting = (int)$this->access->connection->ldapNestedGroups; |
|
291 | + $nesting = (int) $this->access->connection->ldapNestedGroups; |
|
292 | 292 | // depending on the input, we either have a list of DNs or a list of LDAP records |
293 | 293 | // also, the output expects either DNs or records. Testing the first element should suffice. |
294 | 294 | $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]); |
@@ -296,7 +296,7 @@ discard block |
||
296 | 296 | if ($nesting !== 1) { |
297 | 297 | if ($recordMode) { |
298 | 298 | // the keys are numeric, but should hold the DN |
299 | - return array_reduce($list, function ($transformed, $record) use ($dn) { |
|
299 | + return array_reduce($list, function($transformed, $record) use ($dn) { |
|
300 | 300 | if ($record['dn'][0] != $dn) { |
301 | 301 | $transformed[$record['dn'][0]] = $record; |
302 | 302 | } |
@@ -329,7 +329,7 @@ discard block |
||
329 | 329 | * @throws ServerNotAvailableException |
330 | 330 | */ |
331 | 331 | public function gidNumber2Name(string $gid, string $dn) { |
332 | - $cacheKey = 'gidNumberToName' . $gid; |
|
332 | + $cacheKey = 'gidNumberToName'.$gid; |
|
333 | 333 | $groupName = $this->access->connection->getFromCache($cacheKey); |
334 | 334 | if (!is_null($groupName) && isset($groupName)) { |
335 | 335 | return $groupName; |
@@ -339,7 +339,7 @@ discard block |
||
339 | 339 | $filter = $this->access->combineFilterWithAnd([ |
340 | 340 | $this->access->connection->ldapGroupFilter, |
341 | 341 | 'objectClass=posixGroup', |
342 | - $this->access->connection->ldapGidNumber . '=' . $gid |
|
342 | + $this->access->connection->ldapGidNumber.'='.$gid |
|
343 | 343 | ]); |
344 | 344 | return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
345 | 345 | } |
@@ -419,7 +419,7 @@ discard block |
||
419 | 419 | if ($search !== '') { |
420 | 420 | $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
421 | 421 | } |
422 | - $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID; |
|
422 | + $filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID; |
|
423 | 423 | |
424 | 424 | return $this->access->combineFilterWithAnd($filterParts); |
425 | 425 | } |
@@ -489,7 +489,7 @@ discard block |
||
489 | 489 | //we need to get the DN from LDAP |
490 | 490 | $filter = $this->access->combineFilterWithAnd([ |
491 | 491 | $this->access->connection->ldapGroupFilter, |
492 | - 'objectsid=' . $domainObjectSid . '-' . $gid |
|
492 | + 'objectsid='.$domainObjectSid.'-'.$gid |
|
493 | 493 | ]); |
494 | 494 | return $this->getNameOfGroup($filter, $cacheKey) ?? false; |
495 | 495 | } |
@@ -546,7 +546,7 @@ discard block |
||
546 | 546 | if ($search !== '') { |
547 | 547 | $filterParts[] = $this->access->getFilterPartForUserSearch($search); |
548 | 548 | } |
549 | - $filterParts[] = 'primaryGroupID=' . $groupID; |
|
549 | + $filterParts[] = 'primaryGroupID='.$groupID; |
|
550 | 550 | |
551 | 551 | return $this->access->combineFilterWithAnd($filterParts); |
552 | 552 | } |
@@ -588,7 +588,7 @@ discard block |
||
588 | 588 | try { |
589 | 589 | $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); |
590 | 590 | $users = $this->access->countUsers($filter, ['dn'], $limit, $offset); |
591 | - return (int)$users; |
|
591 | + return (int) $users; |
|
592 | 592 | } catch (ServerNotAvailableException $e) { |
593 | 593 | throw $e; |
594 | 594 | } catch (Exception $e) { |
@@ -627,7 +627,7 @@ discard block |
||
627 | 627 | if (!$this->enabled) { |
628 | 628 | return []; |
629 | 629 | } |
630 | - $cacheKey = 'getUserGroups' . $uid; |
|
630 | + $cacheKey = 'getUserGroups'.$uid; |
|
631 | 631 | $userGroups = $this->access->connection->getFromCache($cacheKey); |
632 | 632 | if (!is_null($userGroups)) { |
633 | 633 | return $userGroups; |
@@ -685,8 +685,8 @@ discard block |
||
685 | 685 | // if possible, read out membership via memberOf. It's far faster than |
686 | 686 | // performing a search, which still is a fallback later. |
687 | 687 | // memberof doesn't support memberuid, so skip it here. |
688 | - if ((int)$this->access->connection->hasMemberOfFilterSupport === 1 |
|
689 | - && (int)$this->access->connection->useMemberOfToDetectMembership === 1 |
|
688 | + if ((int) $this->access->connection->hasMemberOfFilterSupport === 1 |
|
689 | + && (int) $this->access->connection->useMemberOfToDetectMembership === 1 |
|
690 | 690 | && $this->ldapGroupMemberAssocAttr !== 'memberuid' |
691 | 691 | && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') { |
692 | 692 | $groupDNs = $this->_getGroupDNsFromMemberOf($userDN); |
@@ -778,7 +778,7 @@ discard block |
||
778 | 778 | } |
779 | 779 | $allGroups = []; |
780 | 780 | $seen[$dn] = true; |
781 | - $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn; |
|
781 | + $filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn; |
|
782 | 782 | |
783 | 783 | if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') { |
784 | 784 | //in this case the member entries are email addresses |
@@ -788,7 +788,7 @@ discard block |
||
788 | 788 | $groups = $this->access->fetchListOfGroups($filter, |
789 | 789 | [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']); |
790 | 790 | if (is_array($groups)) { |
791 | - $fetcher = function ($dn, &$seen) { |
|
791 | + $fetcher = function($dn, &$seen) { |
|
792 | 792 | if (is_array($dn) && isset($dn['dn'][0])) { |
793 | 793 | $dn = $dn['dn'][0]; |
794 | 794 | } |
@@ -824,7 +824,7 @@ discard block |
||
824 | 824 | return []; |
825 | 825 | } |
826 | 826 | $search = $this->access->escapeFilterPart($search, true); |
827 | - $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset; |
|
827 | + $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset; |
|
828 | 828 | // check for cache of the exact query |
829 | 829 | $groupUsers = $this->access->connection->getFromCache($cacheKey); |
830 | 830 | if (!is_null($groupUsers)) { |
@@ -835,7 +835,7 @@ discard block |
||
835 | 835 | $limit = null; |
836 | 836 | } |
837 | 837 | // check for cache of the query without limit and offset |
838 | - $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search); |
|
838 | + $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search); |
|
839 | 839 | if (!is_null($groupUsers)) { |
840 | 840 | $groupUsers = array_slice($groupUsers, $offset, $limit); |
841 | 841 | $this->access->connection->writeToCache($cacheKey, $groupUsers); |
@@ -889,7 +889,7 @@ discard block |
||
889 | 889 | continue; |
890 | 890 | } |
891 | 891 | |
892 | - $cacheKey = 'userExistsOnLDAP' . $uid; |
|
892 | + $cacheKey = 'userExistsOnLDAP'.$uid; |
|
893 | 893 | $userExists = $this->access->connection->getFromCache($cacheKey); |
894 | 894 | if ($userExists === false) { |
895 | 895 | continue; |
@@ -915,7 +915,7 @@ discard block |
||
915 | 915 | |
916 | 916 | $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers)); |
917 | 917 | natsort($groupUsers); |
918 | - $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers); |
|
918 | + $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers); |
|
919 | 919 | $groupUsers = array_slice($groupUsers, $offset, $limit); |
920 | 920 | |
921 | 921 | $this->access->connection->writeToCache($cacheKey, $groupUsers); |
@@ -937,7 +937,7 @@ discard block |
||
937 | 937 | return $this->groupPluginManager->countUsersInGroup($gid, $search); |
938 | 938 | } |
939 | 939 | |
940 | - $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search; |
|
940 | + $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search; |
|
941 | 941 | if (!$this->enabled || !$this->groupExists($gid)) { |
942 | 942 | return false; |
943 | 943 | } |
@@ -1035,7 +1035,7 @@ discard block |
||
1035 | 1035 | if (!$this->enabled) { |
1036 | 1036 | return []; |
1037 | 1037 | } |
1038 | - $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset; |
|
1038 | + $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset; |
|
1039 | 1039 | |
1040 | 1040 | //Check cache before driving unnecessary searches |
1041 | 1041 | $ldap_groups = $this->access->connection->getFromCache($cacheKey); |
@@ -1070,31 +1070,31 @@ discard block |
||
1070 | 1070 | * @throws ServerNotAvailableException |
1071 | 1071 | */ |
1072 | 1072 | public function groupExists($gid) { |
1073 | - $groupExists = $this->access->connection->getFromCache('groupExists' . $gid); |
|
1073 | + $groupExists = $this->access->connection->getFromCache('groupExists'.$gid); |
|
1074 | 1074 | if (!is_null($groupExists)) { |
1075 | - return (bool)$groupExists; |
|
1075 | + return (bool) $groupExists; |
|
1076 | 1076 | } |
1077 | 1077 | |
1078 | 1078 | //getting dn, if false the group does not exist. If dn, it may be mapped |
1079 | 1079 | //only, requires more checking. |
1080 | 1080 | $dn = $this->access->groupname2dn($gid); |
1081 | 1081 | if (!$dn) { |
1082 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1082 | + $this->access->connection->writeToCache('groupExists'.$gid, false); |
|
1083 | 1083 | return false; |
1084 | 1084 | } |
1085 | 1085 | |
1086 | 1086 | if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) { |
1087 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1087 | + $this->access->connection->writeToCache('groupExists'.$gid, false); |
|
1088 | 1088 | return false; |
1089 | 1089 | } |
1090 | 1090 | |
1091 | 1091 | //if group really still exists, we will be able to read its objectClass |
1092 | 1092 | if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) { |
1093 | - $this->access->connection->writeToCache('groupExists' . $gid, false); |
|
1093 | + $this->access->connection->writeToCache('groupExists'.$gid, false); |
|
1094 | 1094 | return false; |
1095 | 1095 | } |
1096 | 1096 | |
1097 | - $this->access->connection->writeToCache('groupExists' . $gid, true); |
|
1097 | + $this->access->connection->writeToCache('groupExists'.$gid, true); |
|
1098 | 1098 | return true; |
1099 | 1099 | } |
1100 | 1100 | |
@@ -1127,7 +1127,7 @@ discard block |
||
1127 | 1127 | * compared with GroupInterface::CREATE_GROUP etc. |
1128 | 1128 | */ |
1129 | 1129 | public function implementsActions($actions) { |
1130 | - return (bool)((GroupInterface::COUNT_USERS | |
|
1130 | + return (bool) ((GroupInterface::COUNT_USERS | |
|
1131 | 1131 | $this->groupPluginManager->getImplementedActions()) & $actions); |
1132 | 1132 | } |
1133 | 1133 | |
@@ -1181,7 +1181,7 @@ discard block |
||
1181 | 1181 | if ($ret = $this->groupPluginManager->deleteGroup($gid)) { |
1182 | 1182 | #delete group in nextcloud internal db |
1183 | 1183 | $this->access->getGroupMapper()->unmap($gid); |
1184 | - $this->access->connection->writeToCache("groupExists" . $gid, false); |
|
1184 | + $this->access->connection->writeToCache("groupExists".$gid, false); |
|
1185 | 1185 | } |
1186 | 1186 | return $ret; |
1187 | 1187 | } |
@@ -1262,7 +1262,7 @@ discard block |
||
1262 | 1262 | return $this->groupPluginManager->getDisplayName($gid); |
1263 | 1263 | } |
1264 | 1264 | |
1265 | - $cacheKey = 'group_getDisplayName' . $gid; |
|
1265 | + $cacheKey = 'group_getDisplayName'.$gid; |
|
1266 | 1266 | if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) { |
1267 | 1267 | return $displayName; |
1268 | 1268 | } |
@@ -42,1318 +42,1318 @@ |
||
42 | 42 | use OCP\ILogger; |
43 | 43 | |
44 | 44 | class Wizard extends LDAPUtility { |
45 | - /** @var \OCP\IL10N */ |
|
46 | - protected static $l; |
|
47 | - protected $access; |
|
48 | - protected $cr; |
|
49 | - protected $configuration; |
|
50 | - protected $result; |
|
51 | - protected $resultCache = []; |
|
52 | - |
|
53 | - public const LRESULT_PROCESSED_OK = 2; |
|
54 | - public const LRESULT_PROCESSED_INVALID = 3; |
|
55 | - public const LRESULT_PROCESSED_SKIP = 4; |
|
56 | - |
|
57 | - public const LFILTER_LOGIN = 2; |
|
58 | - public const LFILTER_USER_LIST = 3; |
|
59 | - public const LFILTER_GROUP_LIST = 4; |
|
60 | - |
|
61 | - public const LFILTER_MODE_ASSISTED = 2; |
|
62 | - public const LFILTER_MODE_RAW = 1; |
|
63 | - |
|
64 | - public const LDAP_NW_TIMEOUT = 4; |
|
65 | - |
|
66 | - /** |
|
67 | - * Constructor |
|
68 | - * @param Configuration $configuration an instance of Configuration |
|
69 | - * @param ILDAPWrapper $ldap an instance of ILDAPWrapper |
|
70 | - * @param Access $access |
|
71 | - */ |
|
72 | - public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) { |
|
73 | - parent::__construct($ldap); |
|
74 | - $this->configuration = $configuration; |
|
75 | - if (is_null(Wizard::$l)) { |
|
76 | - Wizard::$l = \OC::$server->getL10N('user_ldap'); |
|
77 | - } |
|
78 | - $this->access = $access; |
|
79 | - $this->result = new WizardResult(); |
|
80 | - } |
|
81 | - |
|
82 | - public function __destruct() { |
|
83 | - if ($this->result->hasChanges()) { |
|
84 | - $this->configuration->saveConfiguration(); |
|
85 | - } |
|
86 | - } |
|
87 | - |
|
88 | - /** |
|
89 | - * counts entries in the LDAP directory |
|
90 | - * |
|
91 | - * @param string $filter the LDAP search filter |
|
92 | - * @param string $type a string being either 'users' or 'groups'; |
|
93 | - * @return int |
|
94 | - * @throws \Exception |
|
95 | - */ |
|
96 | - public function countEntries(string $filter, string $type): int { |
|
97 | - $reqs = ['ldapHost', 'ldapPort', 'ldapBase']; |
|
98 | - if ($type === 'users') { |
|
99 | - $reqs[] = 'ldapUserFilter'; |
|
100 | - } |
|
101 | - if (!$this->checkRequirements($reqs)) { |
|
102 | - throw new \Exception('Requirements not met', 400); |
|
103 | - } |
|
104 | - |
|
105 | - $attr = ['dn']; // default |
|
106 | - $limit = 1001; |
|
107 | - if ($type === 'groups') { |
|
108 | - $result = $this->access->countGroups($filter, $attr, $limit); |
|
109 | - } elseif ($type === 'users') { |
|
110 | - $result = $this->access->countUsers($filter, $attr, $limit); |
|
111 | - } elseif ($type === 'objects') { |
|
112 | - $result = $this->access->countObjects($limit); |
|
113 | - } else { |
|
114 | - throw new \Exception('Internal error: Invalid object type', 500); |
|
115 | - } |
|
116 | - |
|
117 | - return (int)$result; |
|
118 | - } |
|
119 | - |
|
120 | - /** |
|
121 | - * formats the return value of a count operation to the string to be |
|
122 | - * inserted. |
|
123 | - * |
|
124 | - * @param int $count |
|
125 | - * @return string |
|
126 | - */ |
|
127 | - private function formatCountResult(int $count): string { |
|
128 | - if ($count > 1000) { |
|
129 | - return '> 1000'; |
|
130 | - } |
|
131 | - return (string)$count; |
|
132 | - } |
|
133 | - |
|
134 | - public function countGroups() { |
|
135 | - $filter = $this->configuration->ldapGroupFilter; |
|
136 | - |
|
137 | - if (empty($filter)) { |
|
138 | - $output = self::$l->n('%s group found', '%s groups found', 0, [0]); |
|
139 | - $this->result->addChange('ldap_group_count', $output); |
|
140 | - return $this->result; |
|
141 | - } |
|
142 | - |
|
143 | - try { |
|
144 | - $groupsTotal = $this->countEntries($filter, 'groups'); |
|
145 | - } catch (\Exception $e) { |
|
146 | - //400 can be ignored, 500 is forwarded |
|
147 | - if ($e->getCode() === 500) { |
|
148 | - throw $e; |
|
149 | - } |
|
150 | - return false; |
|
151 | - } |
|
152 | - $output = self::$l->n( |
|
153 | - '%s group found', |
|
154 | - '%s groups found', |
|
155 | - $groupsTotal, |
|
156 | - [$this->formatCountResult($groupsTotal)] |
|
157 | - ); |
|
158 | - $this->result->addChange('ldap_group_count', $output); |
|
159 | - return $this->result; |
|
160 | - } |
|
161 | - |
|
162 | - /** |
|
163 | - * @return WizardResult |
|
164 | - * @throws \Exception |
|
165 | - */ |
|
166 | - public function countUsers() { |
|
167 | - $filter = $this->access->getFilterForUserCount(); |
|
168 | - |
|
169 | - $usersTotal = $this->countEntries($filter, 'users'); |
|
170 | - $output = self::$l->n( |
|
171 | - '%s user found', |
|
172 | - '%s users found', |
|
173 | - $usersTotal, |
|
174 | - [$this->formatCountResult($usersTotal)] |
|
175 | - ); |
|
176 | - $this->result->addChange('ldap_user_count', $output); |
|
177 | - return $this->result; |
|
178 | - } |
|
179 | - |
|
180 | - /** |
|
181 | - * counts any objects in the currently set base dn |
|
182 | - * |
|
183 | - * @return WizardResult |
|
184 | - * @throws \Exception |
|
185 | - */ |
|
186 | - public function countInBaseDN() { |
|
187 | - // we don't need to provide a filter in this case |
|
188 | - $total = $this->countEntries('', 'objects'); |
|
189 | - if ($total === false) { |
|
190 | - throw new \Exception('invalid results received'); |
|
191 | - } |
|
192 | - $this->result->addChange('ldap_test_base', $total); |
|
193 | - return $this->result; |
|
194 | - } |
|
195 | - |
|
196 | - /** |
|
197 | - * counts users with a specified attribute |
|
198 | - * @param string $attr |
|
199 | - * @param bool $existsCheck |
|
200 | - * @return int|bool |
|
201 | - */ |
|
202 | - public function countUsersWithAttribute($attr, $existsCheck = false) { |
|
203 | - if (!$this->checkRequirements(['ldapHost', |
|
204 | - 'ldapPort', |
|
205 | - 'ldapBase', |
|
206 | - 'ldapUserFilter', |
|
207 | - ])) { |
|
208 | - return false; |
|
209 | - } |
|
210 | - |
|
211 | - $filter = $this->access->combineFilterWithAnd([ |
|
212 | - $this->configuration->ldapUserFilter, |
|
213 | - $attr . '=*' |
|
214 | - ]); |
|
215 | - |
|
216 | - $limit = ($existsCheck === false) ? null : 1; |
|
217 | - |
|
218 | - return $this->access->countUsers($filter, ['dn'], $limit); |
|
219 | - } |
|
220 | - |
|
221 | - /** |
|
222 | - * detects the display name attribute. If a setting is already present that |
|
223 | - * returns at least one hit, the detection will be canceled. |
|
224 | - * @return WizardResult|bool |
|
225 | - * @throws \Exception |
|
226 | - */ |
|
227 | - public function detectUserDisplayNameAttribute() { |
|
228 | - if (!$this->checkRequirements(['ldapHost', |
|
229 | - 'ldapPort', |
|
230 | - 'ldapBase', |
|
231 | - 'ldapUserFilter', |
|
232 | - ])) { |
|
233 | - return false; |
|
234 | - } |
|
235 | - |
|
236 | - $attr = $this->configuration->ldapUserDisplayName; |
|
237 | - if ($attr !== '' && $attr !== 'displayName') { |
|
238 | - // most likely not the default value with upper case N, |
|
239 | - // verify it still produces a result |
|
240 | - $count = (int)$this->countUsersWithAttribute($attr, true); |
|
241 | - if ($count > 0) { |
|
242 | - //no change, but we sent it back to make sure the user interface |
|
243 | - //is still correct, even if the ajax call was cancelled meanwhile |
|
244 | - $this->result->addChange('ldap_display_name', $attr); |
|
245 | - return $this->result; |
|
246 | - } |
|
247 | - } |
|
248 | - |
|
249 | - // first attribute that has at least one result wins |
|
250 | - $displayNameAttrs = ['displayname', 'cn']; |
|
251 | - foreach ($displayNameAttrs as $attr) { |
|
252 | - $count = (int)$this->countUsersWithAttribute($attr, true); |
|
253 | - |
|
254 | - if ($count > 0) { |
|
255 | - $this->applyFind('ldap_display_name', $attr); |
|
256 | - return $this->result; |
|
257 | - } |
|
258 | - } |
|
259 | - |
|
260 | - throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.')); |
|
261 | - } |
|
262 | - |
|
263 | - /** |
|
264 | - * detects the most often used email attribute for users applying to the |
|
265 | - * user list filter. If a setting is already present that returns at least |
|
266 | - * one hit, the detection will be canceled. |
|
267 | - * @return WizardResult|bool |
|
268 | - */ |
|
269 | - public function detectEmailAttribute() { |
|
270 | - if (!$this->checkRequirements(['ldapHost', |
|
271 | - 'ldapPort', |
|
272 | - 'ldapBase', |
|
273 | - 'ldapUserFilter', |
|
274 | - ])) { |
|
275 | - return false; |
|
276 | - } |
|
277 | - |
|
278 | - $attr = $this->configuration->ldapEmailAttribute; |
|
279 | - if ($attr !== '') { |
|
280 | - $count = (int)$this->countUsersWithAttribute($attr, true); |
|
281 | - if ($count > 0) { |
|
282 | - return false; |
|
283 | - } |
|
284 | - $writeLog = true; |
|
285 | - } else { |
|
286 | - $writeLog = false; |
|
287 | - } |
|
288 | - |
|
289 | - $emailAttributes = ['mail', 'mailPrimaryAddress']; |
|
290 | - $winner = ''; |
|
291 | - $maxUsers = 0; |
|
292 | - foreach ($emailAttributes as $attr) { |
|
293 | - $count = $this->countUsersWithAttribute($attr); |
|
294 | - if ($count > $maxUsers) { |
|
295 | - $maxUsers = $count; |
|
296 | - $winner = $attr; |
|
297 | - } |
|
298 | - } |
|
299 | - |
|
300 | - if ($winner !== '') { |
|
301 | - $this->applyFind('ldap_email_attr', $winner); |
|
302 | - if ($writeLog) { |
|
303 | - \OCP\Util::writeLog('user_ldap', 'The mail attribute has ' . |
|
304 | - 'automatically been reset, because the original value ' . |
|
305 | - 'did not return any results.', ILogger::INFO); |
|
306 | - } |
|
307 | - } |
|
308 | - |
|
309 | - return $this->result; |
|
310 | - } |
|
311 | - |
|
312 | - /** |
|
313 | - * @return WizardResult |
|
314 | - * @throws \Exception |
|
315 | - */ |
|
316 | - public function determineAttributes() { |
|
317 | - if (!$this->checkRequirements(['ldapHost', |
|
318 | - 'ldapPort', |
|
319 | - 'ldapBase', |
|
320 | - 'ldapUserFilter', |
|
321 | - ])) { |
|
322 | - return false; |
|
323 | - } |
|
324 | - |
|
325 | - $attributes = $this->getUserAttributes(); |
|
326 | - |
|
327 | - natcasesort($attributes); |
|
328 | - $attributes = array_values($attributes); |
|
329 | - |
|
330 | - $this->result->addOptions('ldap_loginfilter_attributes', $attributes); |
|
331 | - |
|
332 | - $selected = $this->configuration->ldapLoginFilterAttributes; |
|
333 | - if (is_array($selected) && !empty($selected)) { |
|
334 | - $this->result->addChange('ldap_loginfilter_attributes', $selected); |
|
335 | - } |
|
336 | - |
|
337 | - return $this->result; |
|
338 | - } |
|
339 | - |
|
340 | - /** |
|
341 | - * detects the available LDAP attributes |
|
342 | - * @return array|false The instance's WizardResult instance |
|
343 | - * @throws \Exception |
|
344 | - */ |
|
345 | - private function getUserAttributes() { |
|
346 | - if (!$this->checkRequirements(['ldapHost', |
|
347 | - 'ldapPort', |
|
348 | - 'ldapBase', |
|
349 | - 'ldapUserFilter', |
|
350 | - ])) { |
|
351 | - return false; |
|
352 | - } |
|
353 | - $cr = $this->getConnection(); |
|
354 | - if (!$cr) { |
|
355 | - throw new \Exception('Could not connect to LDAP'); |
|
356 | - } |
|
357 | - |
|
358 | - $base = $this->configuration->ldapBase[0]; |
|
359 | - $filter = $this->configuration->ldapUserFilter; |
|
360 | - $rr = $this->ldap->search($cr, $base, $filter, [], 1, 1); |
|
361 | - if (!$this->ldap->isResource($rr)) { |
|
362 | - return false; |
|
363 | - } |
|
364 | - $er = $this->ldap->firstEntry($cr, $rr); |
|
365 | - $attributes = $this->ldap->getAttributes($cr, $er); |
|
366 | - $pureAttributes = []; |
|
367 | - for ($i = 0; $i < $attributes['count']; $i++) { |
|
368 | - $pureAttributes[] = $attributes[$i]; |
|
369 | - } |
|
370 | - |
|
371 | - return $pureAttributes; |
|
372 | - } |
|
373 | - |
|
374 | - /** |
|
375 | - * detects the available LDAP groups |
|
376 | - * @return WizardResult|false the instance's WizardResult instance |
|
377 | - */ |
|
378 | - public function determineGroupsForGroups() { |
|
379 | - return $this->determineGroups('ldap_groupfilter_groups', |
|
380 | - 'ldapGroupFilterGroups', |
|
381 | - false); |
|
382 | - } |
|
383 | - |
|
384 | - /** |
|
385 | - * detects the available LDAP groups |
|
386 | - * @return WizardResult|false the instance's WizardResult instance |
|
387 | - */ |
|
388 | - public function determineGroupsForUsers() { |
|
389 | - return $this->determineGroups('ldap_userfilter_groups', |
|
390 | - 'ldapUserFilterGroups'); |
|
391 | - } |
|
392 | - |
|
393 | - /** |
|
394 | - * detects the available LDAP groups |
|
395 | - * @param string $dbKey |
|
396 | - * @param string $confKey |
|
397 | - * @param bool $testMemberOf |
|
398 | - * @return WizardResult|false the instance's WizardResult instance |
|
399 | - * @throws \Exception |
|
400 | - */ |
|
401 | - private function determineGroups($dbKey, $confKey, $testMemberOf = true) { |
|
402 | - if (!$this->checkRequirements(['ldapHost', |
|
403 | - 'ldapPort', |
|
404 | - 'ldapBase', |
|
405 | - ])) { |
|
406 | - return false; |
|
407 | - } |
|
408 | - $cr = $this->getConnection(); |
|
409 | - if (!$cr) { |
|
410 | - throw new \Exception('Could not connect to LDAP'); |
|
411 | - } |
|
412 | - |
|
413 | - $this->fetchGroups($dbKey, $confKey); |
|
414 | - |
|
415 | - if ($testMemberOf) { |
|
416 | - $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf(); |
|
417 | - $this->result->markChange(); |
|
418 | - if (!$this->configuration->hasMemberOfFilterSupport) { |
|
419 | - throw new \Exception('memberOf is not supported by the server'); |
|
420 | - } |
|
421 | - } |
|
422 | - |
|
423 | - return $this->result; |
|
424 | - } |
|
425 | - |
|
426 | - /** |
|
427 | - * fetches all groups from LDAP and adds them to the result object |
|
428 | - * |
|
429 | - * @param string $dbKey |
|
430 | - * @param string $confKey |
|
431 | - * @return array $groupEntries |
|
432 | - * @throws \Exception |
|
433 | - */ |
|
434 | - public function fetchGroups($dbKey, $confKey) { |
|
435 | - $obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames']; |
|
436 | - |
|
437 | - $filterParts = []; |
|
438 | - foreach ($obclasses as $obclass) { |
|
439 | - $filterParts[] = 'objectclass='.$obclass; |
|
440 | - } |
|
441 | - //we filter for everything |
|
442 | - //- that looks like a group and |
|
443 | - //- has the group display name set |
|
444 | - $filter = $this->access->combineFilterWithOr($filterParts); |
|
445 | - $filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']); |
|
446 | - |
|
447 | - $groupNames = []; |
|
448 | - $groupEntries = []; |
|
449 | - $limit = 400; |
|
450 | - $offset = 0; |
|
451 | - do { |
|
452 | - // we need to request dn additionally here, otherwise memberOf |
|
453 | - // detection will fail later |
|
454 | - $result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset); |
|
455 | - foreach ($result as $item) { |
|
456 | - if (!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) { |
|
457 | - // just in case - no issue known |
|
458 | - continue; |
|
459 | - } |
|
460 | - $groupNames[] = $item['cn'][0]; |
|
461 | - $groupEntries[] = $item; |
|
462 | - } |
|
463 | - $offset += $limit; |
|
464 | - } while ($this->access->hasMoreResults()); |
|
465 | - |
|
466 | - if (count($groupNames) > 0) { |
|
467 | - natsort($groupNames); |
|
468 | - $this->result->addOptions($dbKey, array_values($groupNames)); |
|
469 | - } else { |
|
470 | - throw new \Exception(self::$l->t('Could not find the desired feature')); |
|
471 | - } |
|
472 | - |
|
473 | - $setFeatures = $this->configuration->$confKey; |
|
474 | - if (is_array($setFeatures) && !empty($setFeatures)) { |
|
475 | - //something is already configured? pre-select it. |
|
476 | - $this->result->addChange($dbKey, $setFeatures); |
|
477 | - } |
|
478 | - return $groupEntries; |
|
479 | - } |
|
480 | - |
|
481 | - public function determineGroupMemberAssoc() { |
|
482 | - if (!$this->checkRequirements(['ldapHost', |
|
483 | - 'ldapPort', |
|
484 | - 'ldapGroupFilter', |
|
485 | - ])) { |
|
486 | - return false; |
|
487 | - } |
|
488 | - $attribute = $this->detectGroupMemberAssoc(); |
|
489 | - if ($attribute === false) { |
|
490 | - return false; |
|
491 | - } |
|
492 | - $this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]); |
|
493 | - $this->result->addChange('ldap_group_member_assoc_attribute', $attribute); |
|
494 | - |
|
495 | - return $this->result; |
|
496 | - } |
|
497 | - |
|
498 | - /** |
|
499 | - * Detects the available object classes |
|
500 | - * @return WizardResult|false the instance's WizardResult instance |
|
501 | - * @throws \Exception |
|
502 | - */ |
|
503 | - public function determineGroupObjectClasses() { |
|
504 | - if (!$this->checkRequirements(['ldapHost', |
|
505 | - 'ldapPort', |
|
506 | - 'ldapBase', |
|
507 | - ])) { |
|
508 | - return false; |
|
509 | - } |
|
510 | - $cr = $this->getConnection(); |
|
511 | - if (!$cr) { |
|
512 | - throw new \Exception('Could not connect to LDAP'); |
|
513 | - } |
|
514 | - |
|
515 | - $obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*']; |
|
516 | - $this->determineFeature($obclasses, |
|
517 | - 'objectclass', |
|
518 | - 'ldap_groupfilter_objectclass', |
|
519 | - 'ldapGroupFilterObjectclass', |
|
520 | - false); |
|
521 | - |
|
522 | - return $this->result; |
|
523 | - } |
|
524 | - |
|
525 | - /** |
|
526 | - * detects the available object classes |
|
527 | - * @return WizardResult |
|
528 | - * @throws \Exception |
|
529 | - */ |
|
530 | - public function determineUserObjectClasses() { |
|
531 | - if (!$this->checkRequirements(['ldapHost', |
|
532 | - 'ldapPort', |
|
533 | - 'ldapBase', |
|
534 | - ])) { |
|
535 | - return false; |
|
536 | - } |
|
537 | - $cr = $this->getConnection(); |
|
538 | - if (!$cr) { |
|
539 | - throw new \Exception('Could not connect to LDAP'); |
|
540 | - } |
|
541 | - |
|
542 | - $obclasses = ['inetOrgPerson', 'person', 'organizationalPerson', |
|
543 | - 'user', 'posixAccount', '*']; |
|
544 | - $filter = $this->configuration->ldapUserFilter; |
|
545 | - //if filter is empty, it is probably the first time the wizard is called |
|
546 | - //then, apply suggestions. |
|
547 | - $this->determineFeature($obclasses, |
|
548 | - 'objectclass', |
|
549 | - 'ldap_userfilter_objectclass', |
|
550 | - 'ldapUserFilterObjectclass', |
|
551 | - empty($filter)); |
|
552 | - |
|
553 | - return $this->result; |
|
554 | - } |
|
555 | - |
|
556 | - /** |
|
557 | - * @return WizardResult|false |
|
558 | - * @throws \Exception |
|
559 | - */ |
|
560 | - public function getGroupFilter() { |
|
561 | - if (!$this->checkRequirements(['ldapHost', |
|
562 | - 'ldapPort', |
|
563 | - 'ldapBase', |
|
564 | - ])) { |
|
565 | - return false; |
|
566 | - } |
|
567 | - //make sure the use display name is set |
|
568 | - $displayName = $this->configuration->ldapGroupDisplayName; |
|
569 | - if ($displayName === '') { |
|
570 | - $d = $this->configuration->getDefaults(); |
|
571 | - $this->applyFind('ldap_group_display_name', |
|
572 | - $d['ldap_group_display_name']); |
|
573 | - } |
|
574 | - $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST); |
|
575 | - |
|
576 | - $this->applyFind('ldap_group_filter', $filter); |
|
577 | - return $this->result; |
|
578 | - } |
|
579 | - |
|
580 | - /** |
|
581 | - * @return WizardResult|false |
|
582 | - * @throws \Exception |
|
583 | - */ |
|
584 | - public function getUserListFilter() { |
|
585 | - if (!$this->checkRequirements(['ldapHost', |
|
586 | - 'ldapPort', |
|
587 | - 'ldapBase', |
|
588 | - ])) { |
|
589 | - return false; |
|
590 | - } |
|
591 | - //make sure the use display name is set |
|
592 | - $displayName = $this->configuration->ldapUserDisplayName; |
|
593 | - if ($displayName === '') { |
|
594 | - $d = $this->configuration->getDefaults(); |
|
595 | - $this->applyFind('ldap_display_name', $d['ldap_display_name']); |
|
596 | - } |
|
597 | - $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST); |
|
598 | - if (!$filter) { |
|
599 | - throw new \Exception('Cannot create filter'); |
|
600 | - } |
|
601 | - |
|
602 | - $this->applyFind('ldap_userlist_filter', $filter); |
|
603 | - return $this->result; |
|
604 | - } |
|
605 | - |
|
606 | - /** |
|
607 | - * @return bool|WizardResult |
|
608 | - * @throws \Exception |
|
609 | - */ |
|
610 | - public function getUserLoginFilter() { |
|
611 | - if (!$this->checkRequirements(['ldapHost', |
|
612 | - 'ldapPort', |
|
613 | - 'ldapBase', |
|
614 | - 'ldapUserFilter', |
|
615 | - ])) { |
|
616 | - return false; |
|
617 | - } |
|
618 | - |
|
619 | - $filter = $this->composeLdapFilter(self::LFILTER_LOGIN); |
|
620 | - if (!$filter) { |
|
621 | - throw new \Exception('Cannot create filter'); |
|
622 | - } |
|
623 | - |
|
624 | - $this->applyFind('ldap_login_filter', $filter); |
|
625 | - return $this->result; |
|
626 | - } |
|
627 | - |
|
628 | - /** |
|
629 | - * @return bool|WizardResult |
|
630 | - * @param string $loginName |
|
631 | - * @throws \Exception |
|
632 | - */ |
|
633 | - public function testLoginName($loginName) { |
|
634 | - if (!$this->checkRequirements(['ldapHost', |
|
635 | - 'ldapPort', |
|
636 | - 'ldapBase', |
|
637 | - 'ldapLoginFilter', |
|
638 | - ])) { |
|
639 | - return false; |
|
640 | - } |
|
641 | - |
|
642 | - $cr = $this->access->connection->getConnectionResource(); |
|
643 | - if (!$this->ldap->isResource($cr)) { |
|
644 | - throw new \Exception('connection error'); |
|
645 | - } |
|
646 | - |
|
647 | - if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8') |
|
648 | - === false) { |
|
649 | - throw new \Exception('missing placeholder'); |
|
650 | - } |
|
651 | - |
|
652 | - $users = $this->access->countUsersByLoginName($loginName); |
|
653 | - if ($this->ldap->errno($cr) !== 0) { |
|
654 | - throw new \Exception($this->ldap->error($cr)); |
|
655 | - } |
|
656 | - $filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter); |
|
657 | - $this->result->addChange('ldap_test_loginname', $users); |
|
658 | - $this->result->addChange('ldap_test_effective_filter', $filter); |
|
659 | - return $this->result; |
|
660 | - } |
|
661 | - |
|
662 | - /** |
|
663 | - * Tries to determine the port, requires given Host, User DN and Password |
|
664 | - * @return WizardResult|false WizardResult on success, false otherwise |
|
665 | - * @throws \Exception |
|
666 | - */ |
|
667 | - public function guessPortAndTLS() { |
|
668 | - if (!$this->checkRequirements(['ldapHost', |
|
669 | - ])) { |
|
670 | - return false; |
|
671 | - } |
|
672 | - $this->checkHost(); |
|
673 | - $portSettings = $this->getPortSettingsToTry(); |
|
674 | - |
|
675 | - if (!is_array($portSettings)) { |
|
676 | - throw new \Exception(print_r($portSettings, true)); |
|
677 | - } |
|
678 | - |
|
679 | - //proceed from the best configuration and return on first success |
|
680 | - foreach ($portSettings as $setting) { |
|
681 | - $p = $setting['port']; |
|
682 | - $t = $setting['tls']; |
|
683 | - \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG); |
|
684 | - //connectAndBind may throw Exception, it needs to be catched by the |
|
685 | - //callee of this method |
|
686 | - |
|
687 | - try { |
|
688 | - $settingsFound = $this->connectAndBind($p, $t); |
|
689 | - } catch (\Exception $e) { |
|
690 | - // any reply other than -1 (= cannot connect) is already okay, |
|
691 | - // because then we found the server |
|
692 | - // unavailable startTLS returns -11 |
|
693 | - if ($e->getCode() > 0) { |
|
694 | - $settingsFound = true; |
|
695 | - } else { |
|
696 | - throw $e; |
|
697 | - } |
|
698 | - } |
|
699 | - |
|
700 | - if ($settingsFound === true) { |
|
701 | - $config = [ |
|
702 | - 'ldapPort' => $p, |
|
703 | - 'ldapTLS' => (int)$t |
|
704 | - ]; |
|
705 | - $this->configuration->setConfiguration($config); |
|
706 | - \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG); |
|
707 | - $this->result->addChange('ldap_port', $p); |
|
708 | - return $this->result; |
|
709 | - } |
|
710 | - } |
|
711 | - |
|
712 | - //custom port, undetected (we do not brute force) |
|
713 | - return false; |
|
714 | - } |
|
715 | - |
|
716 | - /** |
|
717 | - * tries to determine a base dn from User DN or LDAP Host |
|
718 | - * @return WizardResult|false WizardResult on success, false otherwise |
|
719 | - */ |
|
720 | - public function guessBaseDN() { |
|
721 | - if (!$this->checkRequirements(['ldapHost', |
|
722 | - 'ldapPort', |
|
723 | - ])) { |
|
724 | - return false; |
|
725 | - } |
|
726 | - |
|
727 | - //check whether a DN is given in the agent name (99.9% of all cases) |
|
728 | - $base = null; |
|
729 | - $i = stripos($this->configuration->ldapAgentName, 'dc='); |
|
730 | - if ($i !== false) { |
|
731 | - $base = substr($this->configuration->ldapAgentName, $i); |
|
732 | - if ($this->testBaseDN($base)) { |
|
733 | - $this->applyFind('ldap_base', $base); |
|
734 | - return $this->result; |
|
735 | - } |
|
736 | - } |
|
737 | - |
|
738 | - //this did not help :( |
|
739 | - //Let's see whether we can parse the Host URL and convert the domain to |
|
740 | - //a base DN |
|
741 | - $helper = new Helper(\OC::$server->getConfig()); |
|
742 | - $domain = $helper->getDomainFromURL($this->configuration->ldapHost); |
|
743 | - if (!$domain) { |
|
744 | - return false; |
|
745 | - } |
|
746 | - |
|
747 | - $dparts = explode('.', $domain); |
|
748 | - while (count($dparts) > 0) { |
|
749 | - $base2 = 'dc=' . implode(',dc=', $dparts); |
|
750 | - if ($base !== $base2 && $this->testBaseDN($base2)) { |
|
751 | - $this->applyFind('ldap_base', $base2); |
|
752 | - return $this->result; |
|
753 | - } |
|
754 | - array_shift($dparts); |
|
755 | - } |
|
756 | - |
|
757 | - return false; |
|
758 | - } |
|
759 | - |
|
760 | - /** |
|
761 | - * sets the found value for the configuration key in the WizardResult |
|
762 | - * as well as in the Configuration instance |
|
763 | - * @param string $key the configuration key |
|
764 | - * @param string $value the (detected) value |
|
765 | - * |
|
766 | - */ |
|
767 | - private function applyFind($key, $value) { |
|
768 | - $this->result->addChange($key, $value); |
|
769 | - $this->configuration->setConfiguration([$key => $value]); |
|
770 | - } |
|
771 | - |
|
772 | - /** |
|
773 | - * Checks, whether a port was entered in the Host configuration |
|
774 | - * field. In this case the port will be stripped off, but also stored as |
|
775 | - * setting. |
|
776 | - */ |
|
777 | - private function checkHost() { |
|
778 | - $host = $this->configuration->ldapHost; |
|
779 | - $hostInfo = parse_url($host); |
|
780 | - |
|
781 | - //removes Port from Host |
|
782 | - if (is_array($hostInfo) && isset($hostInfo['port'])) { |
|
783 | - $port = $hostInfo['port']; |
|
784 | - $host = str_replace(':'.$port, '', $host); |
|
785 | - $this->applyFind('ldap_host', $host); |
|
786 | - $this->applyFind('ldap_port', $port); |
|
787 | - } |
|
788 | - } |
|
789 | - |
|
790 | - /** |
|
791 | - * tries to detect the group member association attribute which is |
|
792 | - * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber' |
|
793 | - * @return string|false, string with the attribute name, false on error |
|
794 | - * @throws \Exception |
|
795 | - */ |
|
796 | - private function detectGroupMemberAssoc() { |
|
797 | - $possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress']; |
|
798 | - $filter = $this->configuration->ldapGroupFilter; |
|
799 | - if (empty($filter)) { |
|
800 | - return false; |
|
801 | - } |
|
802 | - $cr = $this->getConnection(); |
|
803 | - if (!$cr) { |
|
804 | - throw new \Exception('Could not connect to LDAP'); |
|
805 | - } |
|
806 | - $base = $this->configuration->ldapBaseGroups[0] ?: $this->configuration->ldapBase[0]; |
|
807 | - $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000); |
|
808 | - if (!$this->ldap->isResource($rr)) { |
|
809 | - return false; |
|
810 | - } |
|
811 | - $er = $this->ldap->firstEntry($cr, $rr); |
|
812 | - while (is_resource($er)) { |
|
813 | - $this->ldap->getDN($cr, $er); |
|
814 | - $attrs = $this->ldap->getAttributes($cr, $er); |
|
815 | - $result = []; |
|
816 | - $possibleAttrsCount = count($possibleAttrs); |
|
817 | - for ($i = 0; $i < $possibleAttrsCount; $i++) { |
|
818 | - if (isset($attrs[$possibleAttrs[$i]])) { |
|
819 | - $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count']; |
|
820 | - } |
|
821 | - } |
|
822 | - if (!empty($result)) { |
|
823 | - natsort($result); |
|
824 | - return key($result); |
|
825 | - } |
|
826 | - |
|
827 | - $er = $this->ldap->nextEntry($cr, $er); |
|
828 | - } |
|
829 | - |
|
830 | - return false; |
|
831 | - } |
|
832 | - |
|
833 | - /** |
|
834 | - * Checks whether for a given BaseDN results will be returned |
|
835 | - * @param string $base the BaseDN to test |
|
836 | - * @return bool true on success, false otherwise |
|
837 | - * @throws \Exception |
|
838 | - */ |
|
839 | - private function testBaseDN($base) { |
|
840 | - $cr = $this->getConnection(); |
|
841 | - if (!$cr) { |
|
842 | - throw new \Exception('Could not connect to LDAP'); |
|
843 | - } |
|
844 | - |
|
845 | - //base is there, let's validate it. If we search for anything, we should |
|
846 | - //get a result set > 0 on a proper base |
|
847 | - $rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1); |
|
848 | - if (!$this->ldap->isResource($rr)) { |
|
849 | - $errorNo = $this->ldap->errno($cr); |
|
850 | - $errorMsg = $this->ldap->error($cr); |
|
851 | - \OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base. |
|
852 | - ' Error '.$errorNo.': '.$errorMsg, ILogger::INFO); |
|
853 | - return false; |
|
854 | - } |
|
855 | - $entries = $this->ldap->countEntries($cr, $rr); |
|
856 | - return ($entries !== false) && ($entries > 0); |
|
857 | - } |
|
858 | - |
|
859 | - /** |
|
860 | - * Checks whether the server supports memberOf in LDAP Filter. |
|
861 | - * Note: at least in OpenLDAP, availability of memberOf is dependent on |
|
862 | - * a configured objectClass. I.e. not necessarily for all available groups |
|
863 | - * memberOf does work. |
|
864 | - * |
|
865 | - * @return bool true if it does, false otherwise |
|
866 | - * @throws \Exception |
|
867 | - */ |
|
868 | - private function testMemberOf() { |
|
869 | - $cr = $this->getConnection(); |
|
870 | - if (!$cr) { |
|
871 | - throw new \Exception('Could not connect to LDAP'); |
|
872 | - } |
|
873 | - $result = $this->access->countUsers('memberOf=*', ['memberOf'], 1); |
|
874 | - if (is_int($result) && $result > 0) { |
|
875 | - return true; |
|
876 | - } |
|
877 | - return false; |
|
878 | - } |
|
879 | - |
|
880 | - /** |
|
881 | - * creates an LDAP Filter from given configuration |
|
882 | - * @param integer $filterType int, for which use case the filter shall be created |
|
883 | - * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or |
|
884 | - * self::LFILTER_GROUP_LIST |
|
885 | - * @return string|false string with the filter on success, false otherwise |
|
886 | - * @throws \Exception |
|
887 | - */ |
|
888 | - private function composeLdapFilter($filterType) { |
|
889 | - $filter = ''; |
|
890 | - $parts = 0; |
|
891 | - switch ($filterType) { |
|
892 | - case self::LFILTER_USER_LIST: |
|
893 | - $objcs = $this->configuration->ldapUserFilterObjectclass; |
|
894 | - //glue objectclasses |
|
895 | - if (is_array($objcs) && count($objcs) > 0) { |
|
896 | - $filter .= '(|'; |
|
897 | - foreach ($objcs as $objc) { |
|
898 | - $filter .= '(objectclass=' . $objc . ')'; |
|
899 | - } |
|
900 | - $filter .= ')'; |
|
901 | - $parts++; |
|
902 | - } |
|
903 | - //glue group memberships |
|
904 | - if ($this->configuration->hasMemberOfFilterSupport) { |
|
905 | - $cns = $this->configuration->ldapUserFilterGroups; |
|
906 | - if (is_array($cns) && count($cns) > 0) { |
|
907 | - $filter .= '(|'; |
|
908 | - $cr = $this->getConnection(); |
|
909 | - if (!$cr) { |
|
910 | - throw new \Exception('Could not connect to LDAP'); |
|
911 | - } |
|
912 | - $base = $this->configuration->ldapBase[0]; |
|
913 | - foreach ($cns as $cn) { |
|
914 | - $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']); |
|
915 | - if (!$this->ldap->isResource($rr)) { |
|
916 | - continue; |
|
917 | - } |
|
918 | - $er = $this->ldap->firstEntry($cr, $rr); |
|
919 | - $attrs = $this->ldap->getAttributes($cr, $er); |
|
920 | - $dn = $this->ldap->getDN($cr, $er); |
|
921 | - if ($dn === false || $dn === '') { |
|
922 | - continue; |
|
923 | - } |
|
924 | - $filterPart = '(memberof=' . $dn . ')'; |
|
925 | - if (isset($attrs['primaryGroupToken'])) { |
|
926 | - $pgt = $attrs['primaryGroupToken'][0]; |
|
927 | - $primaryFilterPart = '(primaryGroupID=' . $pgt .')'; |
|
928 | - $filterPart = '(|' . $filterPart . $primaryFilterPart . ')'; |
|
929 | - } |
|
930 | - $filter .= $filterPart; |
|
931 | - } |
|
932 | - $filter .= ')'; |
|
933 | - } |
|
934 | - $parts++; |
|
935 | - } |
|
936 | - //wrap parts in AND condition |
|
937 | - if ($parts > 1) { |
|
938 | - $filter = '(&' . $filter . ')'; |
|
939 | - } |
|
940 | - if ($filter === '') { |
|
941 | - $filter = '(objectclass=*)'; |
|
942 | - } |
|
943 | - break; |
|
944 | - |
|
945 | - case self::LFILTER_GROUP_LIST: |
|
946 | - $objcs = $this->configuration->ldapGroupFilterObjectclass; |
|
947 | - //glue objectclasses |
|
948 | - if (is_array($objcs) && count($objcs) > 0) { |
|
949 | - $filter .= '(|'; |
|
950 | - foreach ($objcs as $objc) { |
|
951 | - $filter .= '(objectclass=' . $objc . ')'; |
|
952 | - } |
|
953 | - $filter .= ')'; |
|
954 | - $parts++; |
|
955 | - } |
|
956 | - //glue group memberships |
|
957 | - $cns = $this->configuration->ldapGroupFilterGroups; |
|
958 | - if (is_array($cns) && count($cns) > 0) { |
|
959 | - $filter .= '(|'; |
|
960 | - foreach ($cns as $cn) { |
|
961 | - $filter .= '(cn=' . $cn . ')'; |
|
962 | - } |
|
963 | - $filter .= ')'; |
|
964 | - } |
|
965 | - $parts++; |
|
966 | - //wrap parts in AND condition |
|
967 | - if ($parts > 1) { |
|
968 | - $filter = '(&' . $filter . ')'; |
|
969 | - } |
|
970 | - break; |
|
971 | - |
|
972 | - case self::LFILTER_LOGIN: |
|
973 | - $ulf = $this->configuration->ldapUserFilter; |
|
974 | - $loginpart = '=%uid'; |
|
975 | - $filterUsername = ''; |
|
976 | - $userAttributes = $this->getUserAttributes(); |
|
977 | - $userAttributes = array_change_key_case(array_flip($userAttributes)); |
|
978 | - $parts = 0; |
|
979 | - |
|
980 | - if ($this->configuration->ldapLoginFilterUsername === '1') { |
|
981 | - $attr = ''; |
|
982 | - if (isset($userAttributes['uid'])) { |
|
983 | - $attr = 'uid'; |
|
984 | - } elseif (isset($userAttributes['samaccountname'])) { |
|
985 | - $attr = 'samaccountname'; |
|
986 | - } elseif (isset($userAttributes['cn'])) { |
|
987 | - //fallback |
|
988 | - $attr = 'cn'; |
|
989 | - } |
|
990 | - if ($attr !== '') { |
|
991 | - $filterUsername = '(' . $attr . $loginpart . ')'; |
|
992 | - $parts++; |
|
993 | - } |
|
994 | - } |
|
995 | - |
|
996 | - $filterEmail = ''; |
|
997 | - if ($this->configuration->ldapLoginFilterEmail === '1') { |
|
998 | - $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))'; |
|
999 | - $parts++; |
|
1000 | - } |
|
1001 | - |
|
1002 | - $filterAttributes = ''; |
|
1003 | - $attrsToFilter = $this->configuration->ldapLoginFilterAttributes; |
|
1004 | - if (is_array($attrsToFilter) && count($attrsToFilter) > 0) { |
|
1005 | - $filterAttributes = '(|'; |
|
1006 | - foreach ($attrsToFilter as $attribute) { |
|
1007 | - $filterAttributes .= '(' . $attribute . $loginpart . ')'; |
|
1008 | - } |
|
1009 | - $filterAttributes .= ')'; |
|
1010 | - $parts++; |
|
1011 | - } |
|
1012 | - |
|
1013 | - $filterLogin = ''; |
|
1014 | - if ($parts > 1) { |
|
1015 | - $filterLogin = '(|'; |
|
1016 | - } |
|
1017 | - $filterLogin .= $filterUsername; |
|
1018 | - $filterLogin .= $filterEmail; |
|
1019 | - $filterLogin .= $filterAttributes; |
|
1020 | - if ($parts > 1) { |
|
1021 | - $filterLogin .= ')'; |
|
1022 | - } |
|
1023 | - |
|
1024 | - $filter = '(&'.$ulf.$filterLogin.')'; |
|
1025 | - break; |
|
1026 | - } |
|
1027 | - |
|
1028 | - \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG); |
|
1029 | - |
|
1030 | - return $filter; |
|
1031 | - } |
|
1032 | - |
|
1033 | - /** |
|
1034 | - * Connects and Binds to an LDAP Server |
|
1035 | - * |
|
1036 | - * @param int $port the port to connect with |
|
1037 | - * @param bool $tls whether startTLS is to be used |
|
1038 | - * @return bool |
|
1039 | - * @throws \Exception |
|
1040 | - */ |
|
1041 | - private function connectAndBind($port, $tls) { |
|
1042 | - //connect, does not really trigger any server communication |
|
1043 | - $host = $this->configuration->ldapHost; |
|
1044 | - $hostInfo = parse_url($host); |
|
1045 | - if (!$hostInfo) { |
|
1046 | - throw new \Exception(self::$l->t('Invalid Host')); |
|
1047 | - } |
|
1048 | - \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG); |
|
1049 | - $cr = $this->ldap->connect($host, $port); |
|
1050 | - if (!is_resource($cr)) { |
|
1051 | - throw new \Exception(self::$l->t('Invalid Host')); |
|
1052 | - } |
|
1053 | - |
|
1054 | - //set LDAP options |
|
1055 | - $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3); |
|
1056 | - $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0); |
|
1057 | - $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT); |
|
1058 | - |
|
1059 | - try { |
|
1060 | - if ($tls) { |
|
1061 | - $isTlsWorking = @$this->ldap->startTls($cr); |
|
1062 | - if (!$isTlsWorking) { |
|
1063 | - return false; |
|
1064 | - } |
|
1065 | - } |
|
1066 | - |
|
1067 | - \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG); |
|
1068 | - //interesting part: do the bind! |
|
1069 | - $login = $this->ldap->bind($cr, |
|
1070 | - $this->configuration->ldapAgentName, |
|
1071 | - $this->configuration->ldapAgentPassword |
|
1072 | - ); |
|
1073 | - $errNo = $this->ldap->errno($cr); |
|
1074 | - $error = ldap_error($cr); |
|
1075 | - $this->ldap->unbind($cr); |
|
1076 | - } catch (ServerNotAvailableException $e) { |
|
1077 | - return false; |
|
1078 | - } |
|
1079 | - |
|
1080 | - if ($login === true) { |
|
1081 | - $this->ldap->unbind($cr); |
|
1082 | - \OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG); |
|
1083 | - return true; |
|
1084 | - } |
|
1085 | - |
|
1086 | - if ($errNo === -1) { |
|
1087 | - //host, port or TLS wrong |
|
1088 | - return false; |
|
1089 | - } |
|
1090 | - throw new \Exception($error, $errNo); |
|
1091 | - } |
|
1092 | - |
|
1093 | - /** |
|
1094 | - * checks whether a valid combination of agent and password has been |
|
1095 | - * provided (either two values or nothing for anonymous connect) |
|
1096 | - * @return bool, true if everything is fine, false otherwise |
|
1097 | - */ |
|
1098 | - private function checkAgentRequirements() { |
|
1099 | - $agent = $this->configuration->ldapAgentName; |
|
1100 | - $pwd = $this->configuration->ldapAgentPassword; |
|
1101 | - |
|
1102 | - return |
|
1103 | - ($agent !== '' && $pwd !== '') |
|
1104 | - || ($agent === '' && $pwd === '') |
|
1105 | - ; |
|
1106 | - } |
|
1107 | - |
|
1108 | - /** |
|
1109 | - * @param array $reqs |
|
1110 | - * @return bool |
|
1111 | - */ |
|
1112 | - private function checkRequirements($reqs) { |
|
1113 | - $this->checkAgentRequirements(); |
|
1114 | - foreach ($reqs as $option) { |
|
1115 | - $value = $this->configuration->$option; |
|
1116 | - if (empty($value)) { |
|
1117 | - return false; |
|
1118 | - } |
|
1119 | - } |
|
1120 | - return true; |
|
1121 | - } |
|
1122 | - |
|
1123 | - /** |
|
1124 | - * does a cumulativeSearch on LDAP to get different values of a |
|
1125 | - * specified attribute |
|
1126 | - * @param string[] $filters array, the filters that shall be used in the search |
|
1127 | - * @param string $attr the attribute of which a list of values shall be returned |
|
1128 | - * @param int $dnReadLimit the amount of how many DNs should be analyzed. |
|
1129 | - * The lower, the faster |
|
1130 | - * @param string $maxF string. if not null, this variable will have the filter that |
|
1131 | - * yields most result entries |
|
1132 | - * @return array|false an array with the values on success, false otherwise |
|
1133 | - */ |
|
1134 | - public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) { |
|
1135 | - $dnRead = []; |
|
1136 | - $foundItems = []; |
|
1137 | - $maxEntries = 0; |
|
1138 | - if (!is_array($this->configuration->ldapBase) |
|
1139 | - || !isset($this->configuration->ldapBase[0])) { |
|
1140 | - return false; |
|
1141 | - } |
|
1142 | - $base = $this->configuration->ldapBase[0]; |
|
1143 | - $cr = $this->getConnection(); |
|
1144 | - if (!$this->ldap->isResource($cr)) { |
|
1145 | - return false; |
|
1146 | - } |
|
1147 | - $lastFilter = null; |
|
1148 | - if (isset($filters[count($filters)-1])) { |
|
1149 | - $lastFilter = $filters[count($filters)-1]; |
|
1150 | - } |
|
1151 | - foreach ($filters as $filter) { |
|
1152 | - if ($lastFilter === $filter && count($foundItems) > 0) { |
|
1153 | - //skip when the filter is a wildcard and results were found |
|
1154 | - continue; |
|
1155 | - } |
|
1156 | - // 20k limit for performance and reason |
|
1157 | - $rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000); |
|
1158 | - if (!$this->ldap->isResource($rr)) { |
|
1159 | - continue; |
|
1160 | - } |
|
1161 | - $entries = $this->ldap->countEntries($cr, $rr); |
|
1162 | - $getEntryFunc = 'firstEntry'; |
|
1163 | - if (($entries !== false) && ($entries > 0)) { |
|
1164 | - if (!is_null($maxF) && $entries > $maxEntries) { |
|
1165 | - $maxEntries = $entries; |
|
1166 | - $maxF = $filter; |
|
1167 | - } |
|
1168 | - $dnReadCount = 0; |
|
1169 | - do { |
|
1170 | - $entry = $this->ldap->$getEntryFunc($cr, $rr); |
|
1171 | - $getEntryFunc = 'nextEntry'; |
|
1172 | - if (!$this->ldap->isResource($entry)) { |
|
1173 | - continue 2; |
|
1174 | - } |
|
1175 | - $rr = $entry; //will be expected by nextEntry next round |
|
1176 | - $attributes = $this->ldap->getAttributes($cr, $entry); |
|
1177 | - $dn = $this->ldap->getDN($cr, $entry); |
|
1178 | - if ($dn === false || in_array($dn, $dnRead)) { |
|
1179 | - continue; |
|
1180 | - } |
|
1181 | - $newItems = []; |
|
1182 | - $state = $this->getAttributeValuesFromEntry($attributes, |
|
1183 | - $attr, |
|
1184 | - $newItems); |
|
1185 | - $dnReadCount++; |
|
1186 | - $foundItems = array_merge($foundItems, $newItems); |
|
1187 | - $this->resultCache[$dn][$attr] = $newItems; |
|
1188 | - $dnRead[] = $dn; |
|
1189 | - } while (($state === self::LRESULT_PROCESSED_SKIP |
|
1190 | - || $this->ldap->isResource($entry)) |
|
1191 | - && ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit)); |
|
1192 | - } |
|
1193 | - } |
|
1194 | - |
|
1195 | - return array_unique($foundItems); |
|
1196 | - } |
|
1197 | - |
|
1198 | - /** |
|
1199 | - * determines if and which $attr are available on the LDAP server |
|
1200 | - * @param string[] $objectclasses the objectclasses to use as search filter |
|
1201 | - * @param string $attr the attribute to look for |
|
1202 | - * @param string $dbkey the dbkey of the setting the feature is connected to |
|
1203 | - * @param string $confkey the confkey counterpart for the $dbkey as used in the |
|
1204 | - * Configuration class |
|
1205 | - * @param bool $po whether the objectClass with most result entries |
|
1206 | - * shall be pre-selected via the result |
|
1207 | - * @return array|false list of found items. |
|
1208 | - * @throws \Exception |
|
1209 | - */ |
|
1210 | - private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) { |
|
1211 | - $cr = $this->getConnection(); |
|
1212 | - if (!$cr) { |
|
1213 | - throw new \Exception('Could not connect to LDAP'); |
|
1214 | - } |
|
1215 | - $p = 'objectclass='; |
|
1216 | - foreach ($objectclasses as $key => $value) { |
|
1217 | - $objectclasses[$key] = $p.$value; |
|
1218 | - } |
|
1219 | - $maxEntryObjC = ''; |
|
1220 | - |
|
1221 | - //how deep to dig? |
|
1222 | - //When looking for objectclasses, testing few entries is sufficient, |
|
1223 | - $dig = 3; |
|
1224 | - |
|
1225 | - $availableFeatures = |
|
1226 | - $this->cumulativeSearchOnAttribute($objectclasses, $attr, |
|
1227 | - $dig, $maxEntryObjC); |
|
1228 | - if (is_array($availableFeatures) |
|
1229 | - && count($availableFeatures) > 0) { |
|
1230 | - natcasesort($availableFeatures); |
|
1231 | - //natcasesort keeps indices, but we must get rid of them for proper |
|
1232 | - //sorting in the web UI. Therefore: array_values |
|
1233 | - $this->result->addOptions($dbkey, array_values($availableFeatures)); |
|
1234 | - } else { |
|
1235 | - throw new \Exception(self::$l->t('Could not find the desired feature')); |
|
1236 | - } |
|
1237 | - |
|
1238 | - $setFeatures = $this->configuration->$confkey; |
|
1239 | - if (is_array($setFeatures) && !empty($setFeatures)) { |
|
1240 | - //something is already configured? pre-select it. |
|
1241 | - $this->result->addChange($dbkey, $setFeatures); |
|
1242 | - } elseif ($po && $maxEntryObjC !== '') { |
|
1243 | - //pre-select objectclass with most result entries |
|
1244 | - $maxEntryObjC = str_replace($p, '', $maxEntryObjC); |
|
1245 | - $this->applyFind($dbkey, $maxEntryObjC); |
|
1246 | - $this->result->addChange($dbkey, $maxEntryObjC); |
|
1247 | - } |
|
1248 | - |
|
1249 | - return $availableFeatures; |
|
1250 | - } |
|
1251 | - |
|
1252 | - /** |
|
1253 | - * appends a list of values fr |
|
1254 | - * @param resource $result the return value from ldap_get_attributes |
|
1255 | - * @param string $attribute the attribute values to look for |
|
1256 | - * @param array &$known new values will be appended here |
|
1257 | - * @return int, state on of the class constants LRESULT_PROCESSED_OK, |
|
1258 | - * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP |
|
1259 | - */ |
|
1260 | - private function getAttributeValuesFromEntry($result, $attribute, &$known) { |
|
1261 | - if (!is_array($result) |
|
1262 | - || !isset($result['count']) |
|
1263 | - || !$result['count'] > 0) { |
|
1264 | - return self::LRESULT_PROCESSED_INVALID; |
|
1265 | - } |
|
1266 | - |
|
1267 | - // strtolower on all keys for proper comparison |
|
1268 | - $result = \OCP\Util::mb_array_change_key_case($result); |
|
1269 | - $attribute = strtolower($attribute); |
|
1270 | - if (isset($result[$attribute])) { |
|
1271 | - foreach ($result[$attribute] as $key => $val) { |
|
1272 | - if ($key === 'count') { |
|
1273 | - continue; |
|
1274 | - } |
|
1275 | - if (!in_array($val, $known)) { |
|
1276 | - $known[] = $val; |
|
1277 | - } |
|
1278 | - } |
|
1279 | - return self::LRESULT_PROCESSED_OK; |
|
1280 | - } else { |
|
1281 | - return self::LRESULT_PROCESSED_SKIP; |
|
1282 | - } |
|
1283 | - } |
|
1284 | - |
|
1285 | - /** |
|
1286 | - * @return bool|mixed |
|
1287 | - */ |
|
1288 | - private function getConnection() { |
|
1289 | - if (!is_null($this->cr)) { |
|
1290 | - return $this->cr; |
|
1291 | - } |
|
1292 | - |
|
1293 | - $cr = $this->ldap->connect( |
|
1294 | - $this->configuration->ldapHost, |
|
1295 | - $this->configuration->ldapPort |
|
1296 | - ); |
|
1297 | - |
|
1298 | - $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3); |
|
1299 | - $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0); |
|
1300 | - $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT); |
|
1301 | - if ($this->configuration->ldapTLS === 1) { |
|
1302 | - $this->ldap->startTls($cr); |
|
1303 | - } |
|
1304 | - |
|
1305 | - $lo = @$this->ldap->bind($cr, |
|
1306 | - $this->configuration->ldapAgentName, |
|
1307 | - $this->configuration->ldapAgentPassword); |
|
1308 | - if ($lo === true) { |
|
1309 | - $this->$cr = $cr; |
|
1310 | - return $cr; |
|
1311 | - } |
|
1312 | - |
|
1313 | - return false; |
|
1314 | - } |
|
1315 | - |
|
1316 | - /** |
|
1317 | - * @return array |
|
1318 | - */ |
|
1319 | - private function getDefaultLdapPortSettings() { |
|
1320 | - static $settings = [ |
|
1321 | - ['port' => 7636, 'tls' => false], |
|
1322 | - ['port' => 636, 'tls' => false], |
|
1323 | - ['port' => 7389, 'tls' => true], |
|
1324 | - ['port' => 389, 'tls' => true], |
|
1325 | - ['port' => 7389, 'tls' => false], |
|
1326 | - ['port' => 389, 'tls' => false], |
|
1327 | - ]; |
|
1328 | - return $settings; |
|
1329 | - } |
|
1330 | - |
|
1331 | - /** |
|
1332 | - * @return array |
|
1333 | - */ |
|
1334 | - private function getPortSettingsToTry() { |
|
1335 | - //389 ← LDAP / Unencrypted or StartTLS |
|
1336 | - //636 ← LDAPS / SSL |
|
1337 | - //7xxx ← UCS. need to be checked first, because both ports may be open |
|
1338 | - $host = $this->configuration->ldapHost; |
|
1339 | - $port = (int)$this->configuration->ldapPort; |
|
1340 | - $portSettings = []; |
|
1341 | - |
|
1342 | - //In case the port is already provided, we will check this first |
|
1343 | - if ($port > 0) { |
|
1344 | - $hostInfo = parse_url($host); |
|
1345 | - if (!(is_array($hostInfo) |
|
1346 | - && isset($hostInfo['scheme']) |
|
1347 | - && stripos($hostInfo['scheme'], 'ldaps') !== false)) { |
|
1348 | - $portSettings[] = ['port' => $port, 'tls' => true]; |
|
1349 | - } |
|
1350 | - $portSettings[] =['port' => $port, 'tls' => false]; |
|
1351 | - } |
|
1352 | - |
|
1353 | - //default ports |
|
1354 | - $portSettings = array_merge($portSettings, |
|
1355 | - $this->getDefaultLdapPortSettings()); |
|
1356 | - |
|
1357 | - return $portSettings; |
|
1358 | - } |
|
45 | + /** @var \OCP\IL10N */ |
|
46 | + protected static $l; |
|
47 | + protected $access; |
|
48 | + protected $cr; |
|
49 | + protected $configuration; |
|
50 | + protected $result; |
|
51 | + protected $resultCache = []; |
|
52 | + |
|
53 | + public const LRESULT_PROCESSED_OK = 2; |
|
54 | + public const LRESULT_PROCESSED_INVALID = 3; |
|
55 | + public const LRESULT_PROCESSED_SKIP = 4; |
|
56 | + |
|
57 | + public const LFILTER_LOGIN = 2; |
|
58 | + public const LFILTER_USER_LIST = 3; |
|
59 | + public const LFILTER_GROUP_LIST = 4; |
|
60 | + |
|
61 | + public const LFILTER_MODE_ASSISTED = 2; |
|
62 | + public const LFILTER_MODE_RAW = 1; |
|
63 | + |
|
64 | + public const LDAP_NW_TIMEOUT = 4; |
|
65 | + |
|
66 | + /** |
|
67 | + * Constructor |
|
68 | + * @param Configuration $configuration an instance of Configuration |
|
69 | + * @param ILDAPWrapper $ldap an instance of ILDAPWrapper |
|
70 | + * @param Access $access |
|
71 | + */ |
|
72 | + public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) { |
|
73 | + parent::__construct($ldap); |
|
74 | + $this->configuration = $configuration; |
|
75 | + if (is_null(Wizard::$l)) { |
|
76 | + Wizard::$l = \OC::$server->getL10N('user_ldap'); |
|
77 | + } |
|
78 | + $this->access = $access; |
|
79 | + $this->result = new WizardResult(); |
|
80 | + } |
|
81 | + |
|
82 | + public function __destruct() { |
|
83 | + if ($this->result->hasChanges()) { |
|
84 | + $this->configuration->saveConfiguration(); |
|
85 | + } |
|
86 | + } |
|
87 | + |
|
88 | + /** |
|
89 | + * counts entries in the LDAP directory |
|
90 | + * |
|
91 | + * @param string $filter the LDAP search filter |
|
92 | + * @param string $type a string being either 'users' or 'groups'; |
|
93 | + * @return int |
|
94 | + * @throws \Exception |
|
95 | + */ |
|
96 | + public function countEntries(string $filter, string $type): int { |
|
97 | + $reqs = ['ldapHost', 'ldapPort', 'ldapBase']; |
|
98 | + if ($type === 'users') { |
|
99 | + $reqs[] = 'ldapUserFilter'; |
|
100 | + } |
|
101 | + if (!$this->checkRequirements($reqs)) { |
|
102 | + throw new \Exception('Requirements not met', 400); |
|
103 | + } |
|
104 | + |
|
105 | + $attr = ['dn']; // default |
|
106 | + $limit = 1001; |
|
107 | + if ($type === 'groups') { |
|
108 | + $result = $this->access->countGroups($filter, $attr, $limit); |
|
109 | + } elseif ($type === 'users') { |
|
110 | + $result = $this->access->countUsers($filter, $attr, $limit); |
|
111 | + } elseif ($type === 'objects') { |
|
112 | + $result = $this->access->countObjects($limit); |
|
113 | + } else { |
|
114 | + throw new \Exception('Internal error: Invalid object type', 500); |
|
115 | + } |
|
116 | + |
|
117 | + return (int)$result; |
|
118 | + } |
|
119 | + |
|
120 | + /** |
|
121 | + * formats the return value of a count operation to the string to be |
|
122 | + * inserted. |
|
123 | + * |
|
124 | + * @param int $count |
|
125 | + * @return string |
|
126 | + */ |
|
127 | + private function formatCountResult(int $count): string { |
|
128 | + if ($count > 1000) { |
|
129 | + return '> 1000'; |
|
130 | + } |
|
131 | + return (string)$count; |
|
132 | + } |
|
133 | + |
|
134 | + public function countGroups() { |
|
135 | + $filter = $this->configuration->ldapGroupFilter; |
|
136 | + |
|
137 | + if (empty($filter)) { |
|
138 | + $output = self::$l->n('%s group found', '%s groups found', 0, [0]); |
|
139 | + $this->result->addChange('ldap_group_count', $output); |
|
140 | + return $this->result; |
|
141 | + } |
|
142 | + |
|
143 | + try { |
|
144 | + $groupsTotal = $this->countEntries($filter, 'groups'); |
|
145 | + } catch (\Exception $e) { |
|
146 | + //400 can be ignored, 500 is forwarded |
|
147 | + if ($e->getCode() === 500) { |
|
148 | + throw $e; |
|
149 | + } |
|
150 | + return false; |
|
151 | + } |
|
152 | + $output = self::$l->n( |
|
153 | + '%s group found', |
|
154 | + '%s groups found', |
|
155 | + $groupsTotal, |
|
156 | + [$this->formatCountResult($groupsTotal)] |
|
157 | + ); |
|
158 | + $this->result->addChange('ldap_group_count', $output); |
|
159 | + return $this->result; |
|
160 | + } |
|
161 | + |
|
162 | + /** |
|
163 | + * @return WizardResult |
|
164 | + * @throws \Exception |
|
165 | + */ |
|
166 | + public function countUsers() { |
|
167 | + $filter = $this->access->getFilterForUserCount(); |
|
168 | + |
|
169 | + $usersTotal = $this->countEntries($filter, 'users'); |
|
170 | + $output = self::$l->n( |
|
171 | + '%s user found', |
|
172 | + '%s users found', |
|
173 | + $usersTotal, |
|
174 | + [$this->formatCountResult($usersTotal)] |
|
175 | + ); |
|
176 | + $this->result->addChange('ldap_user_count', $output); |
|
177 | + return $this->result; |
|
178 | + } |
|
179 | + |
|
180 | + /** |
|
181 | + * counts any objects in the currently set base dn |
|
182 | + * |
|
183 | + * @return WizardResult |
|
184 | + * @throws \Exception |
|
185 | + */ |
|
186 | + public function countInBaseDN() { |
|
187 | + // we don't need to provide a filter in this case |
|
188 | + $total = $this->countEntries('', 'objects'); |
|
189 | + if ($total === false) { |
|
190 | + throw new \Exception('invalid results received'); |
|
191 | + } |
|
192 | + $this->result->addChange('ldap_test_base', $total); |
|
193 | + return $this->result; |
|
194 | + } |
|
195 | + |
|
196 | + /** |
|
197 | + * counts users with a specified attribute |
|
198 | + * @param string $attr |
|
199 | + * @param bool $existsCheck |
|
200 | + * @return int|bool |
|
201 | + */ |
|
202 | + public function countUsersWithAttribute($attr, $existsCheck = false) { |
|
203 | + if (!$this->checkRequirements(['ldapHost', |
|
204 | + 'ldapPort', |
|
205 | + 'ldapBase', |
|
206 | + 'ldapUserFilter', |
|
207 | + ])) { |
|
208 | + return false; |
|
209 | + } |
|
210 | + |
|
211 | + $filter = $this->access->combineFilterWithAnd([ |
|
212 | + $this->configuration->ldapUserFilter, |
|
213 | + $attr . '=*' |
|
214 | + ]); |
|
215 | + |
|
216 | + $limit = ($existsCheck === false) ? null : 1; |
|
217 | + |
|
218 | + return $this->access->countUsers($filter, ['dn'], $limit); |
|
219 | + } |
|
220 | + |
|
221 | + /** |
|
222 | + * detects the display name attribute. If a setting is already present that |
|
223 | + * returns at least one hit, the detection will be canceled. |
|
224 | + * @return WizardResult|bool |
|
225 | + * @throws \Exception |
|
226 | + */ |
|
227 | + public function detectUserDisplayNameAttribute() { |
|
228 | + if (!$this->checkRequirements(['ldapHost', |
|
229 | + 'ldapPort', |
|
230 | + 'ldapBase', |
|
231 | + 'ldapUserFilter', |
|
232 | + ])) { |
|
233 | + return false; |
|
234 | + } |
|
235 | + |
|
236 | + $attr = $this->configuration->ldapUserDisplayName; |
|
237 | + if ($attr !== '' && $attr !== 'displayName') { |
|
238 | + // most likely not the default value with upper case N, |
|
239 | + // verify it still produces a result |
|
240 | + $count = (int)$this->countUsersWithAttribute($attr, true); |
|
241 | + if ($count > 0) { |
|
242 | + //no change, but we sent it back to make sure the user interface |
|
243 | + //is still correct, even if the ajax call was cancelled meanwhile |
|
244 | + $this->result->addChange('ldap_display_name', $attr); |
|
245 | + return $this->result; |
|
246 | + } |
|
247 | + } |
|
248 | + |
|
249 | + // first attribute that has at least one result wins |
|
250 | + $displayNameAttrs = ['displayname', 'cn']; |
|
251 | + foreach ($displayNameAttrs as $attr) { |
|
252 | + $count = (int)$this->countUsersWithAttribute($attr, true); |
|
253 | + |
|
254 | + if ($count > 0) { |
|
255 | + $this->applyFind('ldap_display_name', $attr); |
|
256 | + return $this->result; |
|
257 | + } |
|
258 | + } |
|
259 | + |
|
260 | + throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.')); |
|
261 | + } |
|
262 | + |
|
263 | + /** |
|
264 | + * detects the most often used email attribute for users applying to the |
|
265 | + * user list filter. If a setting is already present that returns at least |
|
266 | + * one hit, the detection will be canceled. |
|
267 | + * @return WizardResult|bool |
|
268 | + */ |
|
269 | + public function detectEmailAttribute() { |
|
270 | + if (!$this->checkRequirements(['ldapHost', |
|
271 | + 'ldapPort', |
|
272 | + 'ldapBase', |
|
273 | + 'ldapUserFilter', |
|
274 | + ])) { |
|
275 | + return false; |
|
276 | + } |
|
277 | + |
|
278 | + $attr = $this->configuration->ldapEmailAttribute; |
|
279 | + if ($attr !== '') { |
|
280 | + $count = (int)$this->countUsersWithAttribute($attr, true); |
|
281 | + if ($count > 0) { |
|
282 | + return false; |
|
283 | + } |
|
284 | + $writeLog = true; |
|
285 | + } else { |
|
286 | + $writeLog = false; |
|
287 | + } |
|
288 | + |
|
289 | + $emailAttributes = ['mail', 'mailPrimaryAddress']; |
|
290 | + $winner = ''; |
|
291 | + $maxUsers = 0; |
|
292 | + foreach ($emailAttributes as $attr) { |
|
293 | + $count = $this->countUsersWithAttribute($attr); |
|
294 | + if ($count > $maxUsers) { |
|
295 | + $maxUsers = $count; |
|
296 | + $winner = $attr; |
|
297 | + } |
|
298 | + } |
|
299 | + |
|
300 | + if ($winner !== '') { |
|
301 | + $this->applyFind('ldap_email_attr', $winner); |
|
302 | + if ($writeLog) { |
|
303 | + \OCP\Util::writeLog('user_ldap', 'The mail attribute has ' . |
|
304 | + 'automatically been reset, because the original value ' . |
|
305 | + 'did not return any results.', ILogger::INFO); |
|
306 | + } |
|
307 | + } |
|
308 | + |
|
309 | + return $this->result; |
|
310 | + } |
|
311 | + |
|
312 | + /** |
|
313 | + * @return WizardResult |
|
314 | + * @throws \Exception |
|
315 | + */ |
|
316 | + public function determineAttributes() { |
|
317 | + if (!$this->checkRequirements(['ldapHost', |
|
318 | + 'ldapPort', |
|
319 | + 'ldapBase', |
|
320 | + 'ldapUserFilter', |
|
321 | + ])) { |
|
322 | + return false; |
|
323 | + } |
|
324 | + |
|
325 | + $attributes = $this->getUserAttributes(); |
|
326 | + |
|
327 | + natcasesort($attributes); |
|
328 | + $attributes = array_values($attributes); |
|
329 | + |
|
330 | + $this->result->addOptions('ldap_loginfilter_attributes', $attributes); |
|
331 | + |
|
332 | + $selected = $this->configuration->ldapLoginFilterAttributes; |
|
333 | + if (is_array($selected) && !empty($selected)) { |
|
334 | + $this->result->addChange('ldap_loginfilter_attributes', $selected); |
|
335 | + } |
|
336 | + |
|
337 | + return $this->result; |
|
338 | + } |
|
339 | + |
|
340 | + /** |
|
341 | + * detects the available LDAP attributes |
|
342 | + * @return array|false The instance's WizardResult instance |
|
343 | + * @throws \Exception |
|
344 | + */ |
|
345 | + private function getUserAttributes() { |
|
346 | + if (!$this->checkRequirements(['ldapHost', |
|
347 | + 'ldapPort', |
|
348 | + 'ldapBase', |
|
349 | + 'ldapUserFilter', |
|
350 | + ])) { |
|
351 | + return false; |
|
352 | + } |
|
353 | + $cr = $this->getConnection(); |
|
354 | + if (!$cr) { |
|
355 | + throw new \Exception('Could not connect to LDAP'); |
|
356 | + } |
|
357 | + |
|
358 | + $base = $this->configuration->ldapBase[0]; |
|
359 | + $filter = $this->configuration->ldapUserFilter; |
|
360 | + $rr = $this->ldap->search($cr, $base, $filter, [], 1, 1); |
|
361 | + if (!$this->ldap->isResource($rr)) { |
|
362 | + return false; |
|
363 | + } |
|
364 | + $er = $this->ldap->firstEntry($cr, $rr); |
|
365 | + $attributes = $this->ldap->getAttributes($cr, $er); |
|
366 | + $pureAttributes = []; |
|
367 | + for ($i = 0; $i < $attributes['count']; $i++) { |
|
368 | + $pureAttributes[] = $attributes[$i]; |
|
369 | + } |
|
370 | + |
|
371 | + return $pureAttributes; |
|
372 | + } |
|
373 | + |
|
374 | + /** |
|
375 | + * detects the available LDAP groups |
|
376 | + * @return WizardResult|false the instance's WizardResult instance |
|
377 | + */ |
|
378 | + public function determineGroupsForGroups() { |
|
379 | + return $this->determineGroups('ldap_groupfilter_groups', |
|
380 | + 'ldapGroupFilterGroups', |
|
381 | + false); |
|
382 | + } |
|
383 | + |
|
384 | + /** |
|
385 | + * detects the available LDAP groups |
|
386 | + * @return WizardResult|false the instance's WizardResult instance |
|
387 | + */ |
|
388 | + public function determineGroupsForUsers() { |
|
389 | + return $this->determineGroups('ldap_userfilter_groups', |
|
390 | + 'ldapUserFilterGroups'); |
|
391 | + } |
|
392 | + |
|
393 | + /** |
|
394 | + * detects the available LDAP groups |
|
395 | + * @param string $dbKey |
|
396 | + * @param string $confKey |
|
397 | + * @param bool $testMemberOf |
|
398 | + * @return WizardResult|false the instance's WizardResult instance |
|
399 | + * @throws \Exception |
|
400 | + */ |
|
401 | + private function determineGroups($dbKey, $confKey, $testMemberOf = true) { |
|
402 | + if (!$this->checkRequirements(['ldapHost', |
|
403 | + 'ldapPort', |
|
404 | + 'ldapBase', |
|
405 | + ])) { |
|
406 | + return false; |
|
407 | + } |
|
408 | + $cr = $this->getConnection(); |
|
409 | + if (!$cr) { |
|
410 | + throw new \Exception('Could not connect to LDAP'); |
|
411 | + } |
|
412 | + |
|
413 | + $this->fetchGroups($dbKey, $confKey); |
|
414 | + |
|
415 | + if ($testMemberOf) { |
|
416 | + $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf(); |
|
417 | + $this->result->markChange(); |
|
418 | + if (!$this->configuration->hasMemberOfFilterSupport) { |
|
419 | + throw new \Exception('memberOf is not supported by the server'); |
|
420 | + } |
|
421 | + } |
|
422 | + |
|
423 | + return $this->result; |
|
424 | + } |
|
425 | + |
|
426 | + /** |
|
427 | + * fetches all groups from LDAP and adds them to the result object |
|
428 | + * |
|
429 | + * @param string $dbKey |
|
430 | + * @param string $confKey |
|
431 | + * @return array $groupEntries |
|
432 | + * @throws \Exception |
|
433 | + */ |
|
434 | + public function fetchGroups($dbKey, $confKey) { |
|
435 | + $obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames']; |
|
436 | + |
|
437 | + $filterParts = []; |
|
438 | + foreach ($obclasses as $obclass) { |
|
439 | + $filterParts[] = 'objectclass='.$obclass; |
|
440 | + } |
|
441 | + //we filter for everything |
|
442 | + //- that looks like a group and |
|
443 | + //- has the group display name set |
|
444 | + $filter = $this->access->combineFilterWithOr($filterParts); |
|
445 | + $filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']); |
|
446 | + |
|
447 | + $groupNames = []; |
|
448 | + $groupEntries = []; |
|
449 | + $limit = 400; |
|
450 | + $offset = 0; |
|
451 | + do { |
|
452 | + // we need to request dn additionally here, otherwise memberOf |
|
453 | + // detection will fail later |
|
454 | + $result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset); |
|
455 | + foreach ($result as $item) { |
|
456 | + if (!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) { |
|
457 | + // just in case - no issue known |
|
458 | + continue; |
|
459 | + } |
|
460 | + $groupNames[] = $item['cn'][0]; |
|
461 | + $groupEntries[] = $item; |
|
462 | + } |
|
463 | + $offset += $limit; |
|
464 | + } while ($this->access->hasMoreResults()); |
|
465 | + |
|
466 | + if (count($groupNames) > 0) { |
|
467 | + natsort($groupNames); |
|
468 | + $this->result->addOptions($dbKey, array_values($groupNames)); |
|
469 | + } else { |
|
470 | + throw new \Exception(self::$l->t('Could not find the desired feature')); |
|
471 | + } |
|
472 | + |
|
473 | + $setFeatures = $this->configuration->$confKey; |
|
474 | + if (is_array($setFeatures) && !empty($setFeatures)) { |
|
475 | + //something is already configured? pre-select it. |
|
476 | + $this->result->addChange($dbKey, $setFeatures); |
|
477 | + } |
|
478 | + return $groupEntries; |
|
479 | + } |
|
480 | + |
|
481 | + public function determineGroupMemberAssoc() { |
|
482 | + if (!$this->checkRequirements(['ldapHost', |
|
483 | + 'ldapPort', |
|
484 | + 'ldapGroupFilter', |
|
485 | + ])) { |
|
486 | + return false; |
|
487 | + } |
|
488 | + $attribute = $this->detectGroupMemberAssoc(); |
|
489 | + if ($attribute === false) { |
|
490 | + return false; |
|
491 | + } |
|
492 | + $this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]); |
|
493 | + $this->result->addChange('ldap_group_member_assoc_attribute', $attribute); |
|
494 | + |
|
495 | + return $this->result; |
|
496 | + } |
|
497 | + |
|
498 | + /** |
|
499 | + * Detects the available object classes |
|
500 | + * @return WizardResult|false the instance's WizardResult instance |
|
501 | + * @throws \Exception |
|
502 | + */ |
|
503 | + public function determineGroupObjectClasses() { |
|
504 | + if (!$this->checkRequirements(['ldapHost', |
|
505 | + 'ldapPort', |
|
506 | + 'ldapBase', |
|
507 | + ])) { |
|
508 | + return false; |
|
509 | + } |
|
510 | + $cr = $this->getConnection(); |
|
511 | + if (!$cr) { |
|
512 | + throw new \Exception('Could not connect to LDAP'); |
|
513 | + } |
|
514 | + |
|
515 | + $obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*']; |
|
516 | + $this->determineFeature($obclasses, |
|
517 | + 'objectclass', |
|
518 | + 'ldap_groupfilter_objectclass', |
|
519 | + 'ldapGroupFilterObjectclass', |
|
520 | + false); |
|
521 | + |
|
522 | + return $this->result; |
|
523 | + } |
|
524 | + |
|
525 | + /** |
|
526 | + * detects the available object classes |
|
527 | + * @return WizardResult |
|
528 | + * @throws \Exception |
|
529 | + */ |
|
530 | + public function determineUserObjectClasses() { |
|
531 | + if (!$this->checkRequirements(['ldapHost', |
|
532 | + 'ldapPort', |
|
533 | + 'ldapBase', |
|
534 | + ])) { |
|
535 | + return false; |
|
536 | + } |
|
537 | + $cr = $this->getConnection(); |
|
538 | + if (!$cr) { |
|
539 | + throw new \Exception('Could not connect to LDAP'); |
|
540 | + } |
|
541 | + |
|
542 | + $obclasses = ['inetOrgPerson', 'person', 'organizationalPerson', |
|
543 | + 'user', 'posixAccount', '*']; |
|
544 | + $filter = $this->configuration->ldapUserFilter; |
|
545 | + //if filter is empty, it is probably the first time the wizard is called |
|
546 | + //then, apply suggestions. |
|
547 | + $this->determineFeature($obclasses, |
|
548 | + 'objectclass', |
|
549 | + 'ldap_userfilter_objectclass', |
|
550 | + 'ldapUserFilterObjectclass', |
|
551 | + empty($filter)); |
|
552 | + |
|
553 | + return $this->result; |
|
554 | + } |
|
555 | + |
|
556 | + /** |
|
557 | + * @return WizardResult|false |
|
558 | + * @throws \Exception |
|
559 | + */ |
|
560 | + public function getGroupFilter() { |
|
561 | + if (!$this->checkRequirements(['ldapHost', |
|
562 | + 'ldapPort', |
|
563 | + 'ldapBase', |
|
564 | + ])) { |
|
565 | + return false; |
|
566 | + } |
|
567 | + //make sure the use display name is set |
|
568 | + $displayName = $this->configuration->ldapGroupDisplayName; |
|
569 | + if ($displayName === '') { |
|
570 | + $d = $this->configuration->getDefaults(); |
|
571 | + $this->applyFind('ldap_group_display_name', |
|
572 | + $d['ldap_group_display_name']); |
|
573 | + } |
|
574 | + $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST); |
|
575 | + |
|
576 | + $this->applyFind('ldap_group_filter', $filter); |
|
577 | + return $this->result; |
|
578 | + } |
|
579 | + |
|
580 | + /** |
|
581 | + * @return WizardResult|false |
|
582 | + * @throws \Exception |
|
583 | + */ |
|
584 | + public function getUserListFilter() { |
|
585 | + if (!$this->checkRequirements(['ldapHost', |
|
586 | + 'ldapPort', |
|
587 | + 'ldapBase', |
|
588 | + ])) { |
|
589 | + return false; |
|
590 | + } |
|
591 | + //make sure the use display name is set |
|
592 | + $displayName = $this->configuration->ldapUserDisplayName; |
|
593 | + if ($displayName === '') { |
|
594 | + $d = $this->configuration->getDefaults(); |
|
595 | + $this->applyFind('ldap_display_name', $d['ldap_display_name']); |
|
596 | + } |
|
597 | + $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST); |
|
598 | + if (!$filter) { |
|
599 | + throw new \Exception('Cannot create filter'); |
|
600 | + } |
|
601 | + |
|
602 | + $this->applyFind('ldap_userlist_filter', $filter); |
|
603 | + return $this->result; |
|
604 | + } |
|
605 | + |
|
606 | + /** |
|
607 | + * @return bool|WizardResult |
|
608 | + * @throws \Exception |
|
609 | + */ |
|
610 | + public function getUserLoginFilter() { |
|
611 | + if (!$this->checkRequirements(['ldapHost', |
|
612 | + 'ldapPort', |
|
613 | + 'ldapBase', |
|
614 | + 'ldapUserFilter', |
|
615 | + ])) { |
|
616 | + return false; |
|
617 | + } |
|
618 | + |
|
619 | + $filter = $this->composeLdapFilter(self::LFILTER_LOGIN); |
|
620 | + if (!$filter) { |
|
621 | + throw new \Exception('Cannot create filter'); |
|
622 | + } |
|
623 | + |
|
624 | + $this->applyFind('ldap_login_filter', $filter); |
|
625 | + return $this->result; |
|
626 | + } |
|
627 | + |
|
628 | + /** |
|
629 | + * @return bool|WizardResult |
|
630 | + * @param string $loginName |
|
631 | + * @throws \Exception |
|
632 | + */ |
|
633 | + public function testLoginName($loginName) { |
|
634 | + if (!$this->checkRequirements(['ldapHost', |
|
635 | + 'ldapPort', |
|
636 | + 'ldapBase', |
|
637 | + 'ldapLoginFilter', |
|
638 | + ])) { |
|
639 | + return false; |
|
640 | + } |
|
641 | + |
|
642 | + $cr = $this->access->connection->getConnectionResource(); |
|
643 | + if (!$this->ldap->isResource($cr)) { |
|
644 | + throw new \Exception('connection error'); |
|
645 | + } |
|
646 | + |
|
647 | + if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8') |
|
648 | + === false) { |
|
649 | + throw new \Exception('missing placeholder'); |
|
650 | + } |
|
651 | + |
|
652 | + $users = $this->access->countUsersByLoginName($loginName); |
|
653 | + if ($this->ldap->errno($cr) !== 0) { |
|
654 | + throw new \Exception($this->ldap->error($cr)); |
|
655 | + } |
|
656 | + $filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter); |
|
657 | + $this->result->addChange('ldap_test_loginname', $users); |
|
658 | + $this->result->addChange('ldap_test_effective_filter', $filter); |
|
659 | + return $this->result; |
|
660 | + } |
|
661 | + |
|
662 | + /** |
|
663 | + * Tries to determine the port, requires given Host, User DN and Password |
|
664 | + * @return WizardResult|false WizardResult on success, false otherwise |
|
665 | + * @throws \Exception |
|
666 | + */ |
|
667 | + public function guessPortAndTLS() { |
|
668 | + if (!$this->checkRequirements(['ldapHost', |
|
669 | + ])) { |
|
670 | + return false; |
|
671 | + } |
|
672 | + $this->checkHost(); |
|
673 | + $portSettings = $this->getPortSettingsToTry(); |
|
674 | + |
|
675 | + if (!is_array($portSettings)) { |
|
676 | + throw new \Exception(print_r($portSettings, true)); |
|
677 | + } |
|
678 | + |
|
679 | + //proceed from the best configuration and return on first success |
|
680 | + foreach ($portSettings as $setting) { |
|
681 | + $p = $setting['port']; |
|
682 | + $t = $setting['tls']; |
|
683 | + \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG); |
|
684 | + //connectAndBind may throw Exception, it needs to be catched by the |
|
685 | + //callee of this method |
|
686 | + |
|
687 | + try { |
|
688 | + $settingsFound = $this->connectAndBind($p, $t); |
|
689 | + } catch (\Exception $e) { |
|
690 | + // any reply other than -1 (= cannot connect) is already okay, |
|
691 | + // because then we found the server |
|
692 | + // unavailable startTLS returns -11 |
|
693 | + if ($e->getCode() > 0) { |
|
694 | + $settingsFound = true; |
|
695 | + } else { |
|
696 | + throw $e; |
|
697 | + } |
|
698 | + } |
|
699 | + |
|
700 | + if ($settingsFound === true) { |
|
701 | + $config = [ |
|
702 | + 'ldapPort' => $p, |
|
703 | + 'ldapTLS' => (int)$t |
|
704 | + ]; |
|
705 | + $this->configuration->setConfiguration($config); |
|
706 | + \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG); |
|
707 | + $this->result->addChange('ldap_port', $p); |
|
708 | + return $this->result; |
|
709 | + } |
|
710 | + } |
|
711 | + |
|
712 | + //custom port, undetected (we do not brute force) |
|
713 | + return false; |
|
714 | + } |
|
715 | + |
|
716 | + /** |
|
717 | + * tries to determine a base dn from User DN or LDAP Host |
|
718 | + * @return WizardResult|false WizardResult on success, false otherwise |
|
719 | + */ |
|
720 | + public function guessBaseDN() { |
|
721 | + if (!$this->checkRequirements(['ldapHost', |
|
722 | + 'ldapPort', |
|
723 | + ])) { |
|
724 | + return false; |
|
725 | + } |
|
726 | + |
|
727 | + //check whether a DN is given in the agent name (99.9% of all cases) |
|
728 | + $base = null; |
|
729 | + $i = stripos($this->configuration->ldapAgentName, 'dc='); |
|
730 | + if ($i !== false) { |
|
731 | + $base = substr($this->configuration->ldapAgentName, $i); |
|
732 | + if ($this->testBaseDN($base)) { |
|
733 | + $this->applyFind('ldap_base', $base); |
|
734 | + return $this->result; |
|
735 | + } |
|
736 | + } |
|
737 | + |
|
738 | + //this did not help :( |
|
739 | + //Let's see whether we can parse the Host URL and convert the domain to |
|
740 | + //a base DN |
|
741 | + $helper = new Helper(\OC::$server->getConfig()); |
|
742 | + $domain = $helper->getDomainFromURL($this->configuration->ldapHost); |
|
743 | + if (!$domain) { |
|
744 | + return false; |
|
745 | + } |
|
746 | + |
|
747 | + $dparts = explode('.', $domain); |
|
748 | + while (count($dparts) > 0) { |
|
749 | + $base2 = 'dc=' . implode(',dc=', $dparts); |
|
750 | + if ($base !== $base2 && $this->testBaseDN($base2)) { |
|
751 | + $this->applyFind('ldap_base', $base2); |
|
752 | + return $this->result; |
|
753 | + } |
|
754 | + array_shift($dparts); |
|
755 | + } |
|
756 | + |
|
757 | + return false; |
|
758 | + } |
|
759 | + |
|
760 | + /** |
|
761 | + * sets the found value for the configuration key in the WizardResult |
|
762 | + * as well as in the Configuration instance |
|
763 | + * @param string $key the configuration key |
|
764 | + * @param string $value the (detected) value |
|
765 | + * |
|
766 | + */ |
|
767 | + private function applyFind($key, $value) { |
|
768 | + $this->result->addChange($key, $value); |
|
769 | + $this->configuration->setConfiguration([$key => $value]); |
|
770 | + } |
|
771 | + |
|
772 | + /** |
|
773 | + * Checks, whether a port was entered in the Host configuration |
|
774 | + * field. In this case the port will be stripped off, but also stored as |
|
775 | + * setting. |
|
776 | + */ |
|
777 | + private function checkHost() { |
|
778 | + $host = $this->configuration->ldapHost; |
|
779 | + $hostInfo = parse_url($host); |
|
780 | + |
|
781 | + //removes Port from Host |
|
782 | + if (is_array($hostInfo) && isset($hostInfo['port'])) { |
|
783 | + $port = $hostInfo['port']; |
|
784 | + $host = str_replace(':'.$port, '', $host); |
|
785 | + $this->applyFind('ldap_host', $host); |
|
786 | + $this->applyFind('ldap_port', $port); |
|
787 | + } |
|
788 | + } |
|
789 | + |
|
790 | + /** |
|
791 | + * tries to detect the group member association attribute which is |
|
792 | + * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber' |
|
793 | + * @return string|false, string with the attribute name, false on error |
|
794 | + * @throws \Exception |
|
795 | + */ |
|
796 | + private function detectGroupMemberAssoc() { |
|
797 | + $possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress']; |
|
798 | + $filter = $this->configuration->ldapGroupFilter; |
|
799 | + if (empty($filter)) { |
|
800 | + return false; |
|
801 | + } |
|
802 | + $cr = $this->getConnection(); |
|
803 | + if (!$cr) { |
|
804 | + throw new \Exception('Could not connect to LDAP'); |
|
805 | + } |
|
806 | + $base = $this->configuration->ldapBaseGroups[0] ?: $this->configuration->ldapBase[0]; |
|
807 | + $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000); |
|
808 | + if (!$this->ldap->isResource($rr)) { |
|
809 | + return false; |
|
810 | + } |
|
811 | + $er = $this->ldap->firstEntry($cr, $rr); |
|
812 | + while (is_resource($er)) { |
|
813 | + $this->ldap->getDN($cr, $er); |
|
814 | + $attrs = $this->ldap->getAttributes($cr, $er); |
|
815 | + $result = []; |
|
816 | + $possibleAttrsCount = count($possibleAttrs); |
|
817 | + for ($i = 0; $i < $possibleAttrsCount; $i++) { |
|
818 | + if (isset($attrs[$possibleAttrs[$i]])) { |
|
819 | + $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count']; |
|
820 | + } |
|
821 | + } |
|
822 | + if (!empty($result)) { |
|
823 | + natsort($result); |
|
824 | + return key($result); |
|
825 | + } |
|
826 | + |
|
827 | + $er = $this->ldap->nextEntry($cr, $er); |
|
828 | + } |
|
829 | + |
|
830 | + return false; |
|
831 | + } |
|
832 | + |
|
833 | + /** |
|
834 | + * Checks whether for a given BaseDN results will be returned |
|
835 | + * @param string $base the BaseDN to test |
|
836 | + * @return bool true on success, false otherwise |
|
837 | + * @throws \Exception |
|
838 | + */ |
|
839 | + private function testBaseDN($base) { |
|
840 | + $cr = $this->getConnection(); |
|
841 | + if (!$cr) { |
|
842 | + throw new \Exception('Could not connect to LDAP'); |
|
843 | + } |
|
844 | + |
|
845 | + //base is there, let's validate it. If we search for anything, we should |
|
846 | + //get a result set > 0 on a proper base |
|
847 | + $rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1); |
|
848 | + if (!$this->ldap->isResource($rr)) { |
|
849 | + $errorNo = $this->ldap->errno($cr); |
|
850 | + $errorMsg = $this->ldap->error($cr); |
|
851 | + \OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base. |
|
852 | + ' Error '.$errorNo.': '.$errorMsg, ILogger::INFO); |
|
853 | + return false; |
|
854 | + } |
|
855 | + $entries = $this->ldap->countEntries($cr, $rr); |
|
856 | + return ($entries !== false) && ($entries > 0); |
|
857 | + } |
|
858 | + |
|
859 | + /** |
|
860 | + * Checks whether the server supports memberOf in LDAP Filter. |
|
861 | + * Note: at least in OpenLDAP, availability of memberOf is dependent on |
|
862 | + * a configured objectClass. I.e. not necessarily for all available groups |
|
863 | + * memberOf does work. |
|
864 | + * |
|
865 | + * @return bool true if it does, false otherwise |
|
866 | + * @throws \Exception |
|
867 | + */ |
|
868 | + private function testMemberOf() { |
|
869 | + $cr = $this->getConnection(); |
|
870 | + if (!$cr) { |
|
871 | + throw new \Exception('Could not connect to LDAP'); |
|
872 | + } |
|
873 | + $result = $this->access->countUsers('memberOf=*', ['memberOf'], 1); |
|
874 | + if (is_int($result) && $result > 0) { |
|
875 | + return true; |
|
876 | + } |
|
877 | + return false; |
|
878 | + } |
|
879 | + |
|
880 | + /** |
|
881 | + * creates an LDAP Filter from given configuration |
|
882 | + * @param integer $filterType int, for which use case the filter shall be created |
|
883 | + * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or |
|
884 | + * self::LFILTER_GROUP_LIST |
|
885 | + * @return string|false string with the filter on success, false otherwise |
|
886 | + * @throws \Exception |
|
887 | + */ |
|
888 | + private function composeLdapFilter($filterType) { |
|
889 | + $filter = ''; |
|
890 | + $parts = 0; |
|
891 | + switch ($filterType) { |
|
892 | + case self::LFILTER_USER_LIST: |
|
893 | + $objcs = $this->configuration->ldapUserFilterObjectclass; |
|
894 | + //glue objectclasses |
|
895 | + if (is_array($objcs) && count($objcs) > 0) { |
|
896 | + $filter .= '(|'; |
|
897 | + foreach ($objcs as $objc) { |
|
898 | + $filter .= '(objectclass=' . $objc . ')'; |
|
899 | + } |
|
900 | + $filter .= ')'; |
|
901 | + $parts++; |
|
902 | + } |
|
903 | + //glue group memberships |
|
904 | + if ($this->configuration->hasMemberOfFilterSupport) { |
|
905 | + $cns = $this->configuration->ldapUserFilterGroups; |
|
906 | + if (is_array($cns) && count($cns) > 0) { |
|
907 | + $filter .= '(|'; |
|
908 | + $cr = $this->getConnection(); |
|
909 | + if (!$cr) { |
|
910 | + throw new \Exception('Could not connect to LDAP'); |
|
911 | + } |
|
912 | + $base = $this->configuration->ldapBase[0]; |
|
913 | + foreach ($cns as $cn) { |
|
914 | + $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']); |
|
915 | + if (!$this->ldap->isResource($rr)) { |
|
916 | + continue; |
|
917 | + } |
|
918 | + $er = $this->ldap->firstEntry($cr, $rr); |
|
919 | + $attrs = $this->ldap->getAttributes($cr, $er); |
|
920 | + $dn = $this->ldap->getDN($cr, $er); |
|
921 | + if ($dn === false || $dn === '') { |
|
922 | + continue; |
|
923 | + } |
|
924 | + $filterPart = '(memberof=' . $dn . ')'; |
|
925 | + if (isset($attrs['primaryGroupToken'])) { |
|
926 | + $pgt = $attrs['primaryGroupToken'][0]; |
|
927 | + $primaryFilterPart = '(primaryGroupID=' . $pgt .')'; |
|
928 | + $filterPart = '(|' . $filterPart . $primaryFilterPart . ')'; |
|
929 | + } |
|
930 | + $filter .= $filterPart; |
|
931 | + } |
|
932 | + $filter .= ')'; |
|
933 | + } |
|
934 | + $parts++; |
|
935 | + } |
|
936 | + //wrap parts in AND condition |
|
937 | + if ($parts > 1) { |
|
938 | + $filter = '(&' . $filter . ')'; |
|
939 | + } |
|
940 | + if ($filter === '') { |
|
941 | + $filter = '(objectclass=*)'; |
|
942 | + } |
|
943 | + break; |
|
944 | + |
|
945 | + case self::LFILTER_GROUP_LIST: |
|
946 | + $objcs = $this->configuration->ldapGroupFilterObjectclass; |
|
947 | + //glue objectclasses |
|
948 | + if (is_array($objcs) && count($objcs) > 0) { |
|
949 | + $filter .= '(|'; |
|
950 | + foreach ($objcs as $objc) { |
|
951 | + $filter .= '(objectclass=' . $objc . ')'; |
|
952 | + } |
|
953 | + $filter .= ')'; |
|
954 | + $parts++; |
|
955 | + } |
|
956 | + //glue group memberships |
|
957 | + $cns = $this->configuration->ldapGroupFilterGroups; |
|
958 | + if (is_array($cns) && count($cns) > 0) { |
|
959 | + $filter .= '(|'; |
|
960 | + foreach ($cns as $cn) { |
|
961 | + $filter .= '(cn=' . $cn . ')'; |
|
962 | + } |
|
963 | + $filter .= ')'; |
|
964 | + } |
|
965 | + $parts++; |
|
966 | + //wrap parts in AND condition |
|
967 | + if ($parts > 1) { |
|
968 | + $filter = '(&' . $filter . ')'; |
|
969 | + } |
|
970 | + break; |
|
971 | + |
|
972 | + case self::LFILTER_LOGIN: |
|
973 | + $ulf = $this->configuration->ldapUserFilter; |
|
974 | + $loginpart = '=%uid'; |
|
975 | + $filterUsername = ''; |
|
976 | + $userAttributes = $this->getUserAttributes(); |
|
977 | + $userAttributes = array_change_key_case(array_flip($userAttributes)); |
|
978 | + $parts = 0; |
|
979 | + |
|
980 | + if ($this->configuration->ldapLoginFilterUsername === '1') { |
|
981 | + $attr = ''; |
|
982 | + if (isset($userAttributes['uid'])) { |
|
983 | + $attr = 'uid'; |
|
984 | + } elseif (isset($userAttributes['samaccountname'])) { |
|
985 | + $attr = 'samaccountname'; |
|
986 | + } elseif (isset($userAttributes['cn'])) { |
|
987 | + //fallback |
|
988 | + $attr = 'cn'; |
|
989 | + } |
|
990 | + if ($attr !== '') { |
|
991 | + $filterUsername = '(' . $attr . $loginpart . ')'; |
|
992 | + $parts++; |
|
993 | + } |
|
994 | + } |
|
995 | + |
|
996 | + $filterEmail = ''; |
|
997 | + if ($this->configuration->ldapLoginFilterEmail === '1') { |
|
998 | + $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))'; |
|
999 | + $parts++; |
|
1000 | + } |
|
1001 | + |
|
1002 | + $filterAttributes = ''; |
|
1003 | + $attrsToFilter = $this->configuration->ldapLoginFilterAttributes; |
|
1004 | + if (is_array($attrsToFilter) && count($attrsToFilter) > 0) { |
|
1005 | + $filterAttributes = '(|'; |
|
1006 | + foreach ($attrsToFilter as $attribute) { |
|
1007 | + $filterAttributes .= '(' . $attribute . $loginpart . ')'; |
|
1008 | + } |
|
1009 | + $filterAttributes .= ')'; |
|
1010 | + $parts++; |
|
1011 | + } |
|
1012 | + |
|
1013 | + $filterLogin = ''; |
|
1014 | + if ($parts > 1) { |
|
1015 | + $filterLogin = '(|'; |
|
1016 | + } |
|
1017 | + $filterLogin .= $filterUsername; |
|
1018 | + $filterLogin .= $filterEmail; |
|
1019 | + $filterLogin .= $filterAttributes; |
|
1020 | + if ($parts > 1) { |
|
1021 | + $filterLogin .= ')'; |
|
1022 | + } |
|
1023 | + |
|
1024 | + $filter = '(&'.$ulf.$filterLogin.')'; |
|
1025 | + break; |
|
1026 | + } |
|
1027 | + |
|
1028 | + \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG); |
|
1029 | + |
|
1030 | + return $filter; |
|
1031 | + } |
|
1032 | + |
|
1033 | + /** |
|
1034 | + * Connects and Binds to an LDAP Server |
|
1035 | + * |
|
1036 | + * @param int $port the port to connect with |
|
1037 | + * @param bool $tls whether startTLS is to be used |
|
1038 | + * @return bool |
|
1039 | + * @throws \Exception |
|
1040 | + */ |
|
1041 | + private function connectAndBind($port, $tls) { |
|
1042 | + //connect, does not really trigger any server communication |
|
1043 | + $host = $this->configuration->ldapHost; |
|
1044 | + $hostInfo = parse_url($host); |
|
1045 | + if (!$hostInfo) { |
|
1046 | + throw new \Exception(self::$l->t('Invalid Host')); |
|
1047 | + } |
|
1048 | + \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG); |
|
1049 | + $cr = $this->ldap->connect($host, $port); |
|
1050 | + if (!is_resource($cr)) { |
|
1051 | + throw new \Exception(self::$l->t('Invalid Host')); |
|
1052 | + } |
|
1053 | + |
|
1054 | + //set LDAP options |
|
1055 | + $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3); |
|
1056 | + $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0); |
|
1057 | + $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT); |
|
1058 | + |
|
1059 | + try { |
|
1060 | + if ($tls) { |
|
1061 | + $isTlsWorking = @$this->ldap->startTls($cr); |
|
1062 | + if (!$isTlsWorking) { |
|
1063 | + return false; |
|
1064 | + } |
|
1065 | + } |
|
1066 | + |
|
1067 | + \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG); |
|
1068 | + //interesting part: do the bind! |
|
1069 | + $login = $this->ldap->bind($cr, |
|
1070 | + $this->configuration->ldapAgentName, |
|
1071 | + $this->configuration->ldapAgentPassword |
|
1072 | + ); |
|
1073 | + $errNo = $this->ldap->errno($cr); |
|
1074 | + $error = ldap_error($cr); |
|
1075 | + $this->ldap->unbind($cr); |
|
1076 | + } catch (ServerNotAvailableException $e) { |
|
1077 | + return false; |
|
1078 | + } |
|
1079 | + |
|
1080 | + if ($login === true) { |
|
1081 | + $this->ldap->unbind($cr); |
|
1082 | + \OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG); |
|
1083 | + return true; |
|
1084 | + } |
|
1085 | + |
|
1086 | + if ($errNo === -1) { |
|
1087 | + //host, port or TLS wrong |
|
1088 | + return false; |
|
1089 | + } |
|
1090 | + throw new \Exception($error, $errNo); |
|
1091 | + } |
|
1092 | + |
|
1093 | + /** |
|
1094 | + * checks whether a valid combination of agent and password has been |
|
1095 | + * provided (either two values or nothing for anonymous connect) |
|
1096 | + * @return bool, true if everything is fine, false otherwise |
|
1097 | + */ |
|
1098 | + private function checkAgentRequirements() { |
|
1099 | + $agent = $this->configuration->ldapAgentName; |
|
1100 | + $pwd = $this->configuration->ldapAgentPassword; |
|
1101 | + |
|
1102 | + return |
|
1103 | + ($agent !== '' && $pwd !== '') |
|
1104 | + || ($agent === '' && $pwd === '') |
|
1105 | + ; |
|
1106 | + } |
|
1107 | + |
|
1108 | + /** |
|
1109 | + * @param array $reqs |
|
1110 | + * @return bool |
|
1111 | + */ |
|
1112 | + private function checkRequirements($reqs) { |
|
1113 | + $this->checkAgentRequirements(); |
|
1114 | + foreach ($reqs as $option) { |
|
1115 | + $value = $this->configuration->$option; |
|
1116 | + if (empty($value)) { |
|
1117 | + return false; |
|
1118 | + } |
|
1119 | + } |
|
1120 | + return true; |
|
1121 | + } |
|
1122 | + |
|
1123 | + /** |
|
1124 | + * does a cumulativeSearch on LDAP to get different values of a |
|
1125 | + * specified attribute |
|
1126 | + * @param string[] $filters array, the filters that shall be used in the search |
|
1127 | + * @param string $attr the attribute of which a list of values shall be returned |
|
1128 | + * @param int $dnReadLimit the amount of how many DNs should be analyzed. |
|
1129 | + * The lower, the faster |
|
1130 | + * @param string $maxF string. if not null, this variable will have the filter that |
|
1131 | + * yields most result entries |
|
1132 | + * @return array|false an array with the values on success, false otherwise |
|
1133 | + */ |
|
1134 | + public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) { |
|
1135 | + $dnRead = []; |
|
1136 | + $foundItems = []; |
|
1137 | + $maxEntries = 0; |
|
1138 | + if (!is_array($this->configuration->ldapBase) |
|
1139 | + || !isset($this->configuration->ldapBase[0])) { |
|
1140 | + return false; |
|
1141 | + } |
|
1142 | + $base = $this->configuration->ldapBase[0]; |
|
1143 | + $cr = $this->getConnection(); |
|
1144 | + if (!$this->ldap->isResource($cr)) { |
|
1145 | + return false; |
|
1146 | + } |
|
1147 | + $lastFilter = null; |
|
1148 | + if (isset($filters[count($filters)-1])) { |
|
1149 | + $lastFilter = $filters[count($filters)-1]; |
|
1150 | + } |
|
1151 | + foreach ($filters as $filter) { |
|
1152 | + if ($lastFilter === $filter && count($foundItems) > 0) { |
|
1153 | + //skip when the filter is a wildcard and results were found |
|
1154 | + continue; |
|
1155 | + } |
|
1156 | + // 20k limit for performance and reason |
|
1157 | + $rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000); |
|
1158 | + if (!$this->ldap->isResource($rr)) { |
|
1159 | + continue; |
|
1160 | + } |
|
1161 | + $entries = $this->ldap->countEntries($cr, $rr); |
|
1162 | + $getEntryFunc = 'firstEntry'; |
|
1163 | + if (($entries !== false) && ($entries > 0)) { |
|
1164 | + if (!is_null($maxF) && $entries > $maxEntries) { |
|
1165 | + $maxEntries = $entries; |
|
1166 | + $maxF = $filter; |
|
1167 | + } |
|
1168 | + $dnReadCount = 0; |
|
1169 | + do { |
|
1170 | + $entry = $this->ldap->$getEntryFunc($cr, $rr); |
|
1171 | + $getEntryFunc = 'nextEntry'; |
|
1172 | + if (!$this->ldap->isResource($entry)) { |
|
1173 | + continue 2; |
|
1174 | + } |
|
1175 | + $rr = $entry; //will be expected by nextEntry next round |
|
1176 | + $attributes = $this->ldap->getAttributes($cr, $entry); |
|
1177 | + $dn = $this->ldap->getDN($cr, $entry); |
|
1178 | + if ($dn === false || in_array($dn, $dnRead)) { |
|
1179 | + continue; |
|
1180 | + } |
|
1181 | + $newItems = []; |
|
1182 | + $state = $this->getAttributeValuesFromEntry($attributes, |
|
1183 | + $attr, |
|
1184 | + $newItems); |
|
1185 | + $dnReadCount++; |
|
1186 | + $foundItems = array_merge($foundItems, $newItems); |
|
1187 | + $this->resultCache[$dn][$attr] = $newItems; |
|
1188 | + $dnRead[] = $dn; |
|
1189 | + } while (($state === self::LRESULT_PROCESSED_SKIP |
|
1190 | + || $this->ldap->isResource($entry)) |
|
1191 | + && ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit)); |
|
1192 | + } |
|
1193 | + } |
|
1194 | + |
|
1195 | + return array_unique($foundItems); |
|
1196 | + } |
|
1197 | + |
|
1198 | + /** |
|
1199 | + * determines if and which $attr are available on the LDAP server |
|
1200 | + * @param string[] $objectclasses the objectclasses to use as search filter |
|
1201 | + * @param string $attr the attribute to look for |
|
1202 | + * @param string $dbkey the dbkey of the setting the feature is connected to |
|
1203 | + * @param string $confkey the confkey counterpart for the $dbkey as used in the |
|
1204 | + * Configuration class |
|
1205 | + * @param bool $po whether the objectClass with most result entries |
|
1206 | + * shall be pre-selected via the result |
|
1207 | + * @return array|false list of found items. |
|
1208 | + * @throws \Exception |
|
1209 | + */ |
|
1210 | + private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) { |
|
1211 | + $cr = $this->getConnection(); |
|
1212 | + if (!$cr) { |
|
1213 | + throw new \Exception('Could not connect to LDAP'); |
|
1214 | + } |
|
1215 | + $p = 'objectclass='; |
|
1216 | + foreach ($objectclasses as $key => $value) { |
|
1217 | + $objectclasses[$key] = $p.$value; |
|
1218 | + } |
|
1219 | + $maxEntryObjC = ''; |
|
1220 | + |
|
1221 | + //how deep to dig? |
|
1222 | + //When looking for objectclasses, testing few entries is sufficient, |
|
1223 | + $dig = 3; |
|
1224 | + |
|
1225 | + $availableFeatures = |
|
1226 | + $this->cumulativeSearchOnAttribute($objectclasses, $attr, |
|
1227 | + $dig, $maxEntryObjC); |
|
1228 | + if (is_array($availableFeatures) |
|
1229 | + && count($availableFeatures) > 0) { |
|
1230 | + natcasesort($availableFeatures); |
|
1231 | + //natcasesort keeps indices, but we must get rid of them for proper |
|
1232 | + //sorting in the web UI. Therefore: array_values |
|
1233 | + $this->result->addOptions($dbkey, array_values($availableFeatures)); |
|
1234 | + } else { |
|
1235 | + throw new \Exception(self::$l->t('Could not find the desired feature')); |
|
1236 | + } |
|
1237 | + |
|
1238 | + $setFeatures = $this->configuration->$confkey; |
|
1239 | + if (is_array($setFeatures) && !empty($setFeatures)) { |
|
1240 | + //something is already configured? pre-select it. |
|
1241 | + $this->result->addChange($dbkey, $setFeatures); |
|
1242 | + } elseif ($po && $maxEntryObjC !== '') { |
|
1243 | + //pre-select objectclass with most result entries |
|
1244 | + $maxEntryObjC = str_replace($p, '', $maxEntryObjC); |
|
1245 | + $this->applyFind($dbkey, $maxEntryObjC); |
|
1246 | + $this->result->addChange($dbkey, $maxEntryObjC); |
|
1247 | + } |
|
1248 | + |
|
1249 | + return $availableFeatures; |
|
1250 | + } |
|
1251 | + |
|
1252 | + /** |
|
1253 | + * appends a list of values fr |
|
1254 | + * @param resource $result the return value from ldap_get_attributes |
|
1255 | + * @param string $attribute the attribute values to look for |
|
1256 | + * @param array &$known new values will be appended here |
|
1257 | + * @return int, state on of the class constants LRESULT_PROCESSED_OK, |
|
1258 | + * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP |
|
1259 | + */ |
|
1260 | + private function getAttributeValuesFromEntry($result, $attribute, &$known) { |
|
1261 | + if (!is_array($result) |
|
1262 | + || !isset($result['count']) |
|
1263 | + || !$result['count'] > 0) { |
|
1264 | + return self::LRESULT_PROCESSED_INVALID; |
|
1265 | + } |
|
1266 | + |
|
1267 | + // strtolower on all keys for proper comparison |
|
1268 | + $result = \OCP\Util::mb_array_change_key_case($result); |
|
1269 | + $attribute = strtolower($attribute); |
|
1270 | + if (isset($result[$attribute])) { |
|
1271 | + foreach ($result[$attribute] as $key => $val) { |
|
1272 | + if ($key === 'count') { |
|
1273 | + continue; |
|
1274 | + } |
|
1275 | + if (!in_array($val, $known)) { |
|
1276 | + $known[] = $val; |
|
1277 | + } |
|
1278 | + } |
|
1279 | + return self::LRESULT_PROCESSED_OK; |
|
1280 | + } else { |
|
1281 | + return self::LRESULT_PROCESSED_SKIP; |
|
1282 | + } |
|
1283 | + } |
|
1284 | + |
|
1285 | + /** |
|
1286 | + * @return bool|mixed |
|
1287 | + */ |
|
1288 | + private function getConnection() { |
|
1289 | + if (!is_null($this->cr)) { |
|
1290 | + return $this->cr; |
|
1291 | + } |
|
1292 | + |
|
1293 | + $cr = $this->ldap->connect( |
|
1294 | + $this->configuration->ldapHost, |
|
1295 | + $this->configuration->ldapPort |
|
1296 | + ); |
|
1297 | + |
|
1298 | + $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3); |
|
1299 | + $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0); |
|
1300 | + $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT); |
|
1301 | + if ($this->configuration->ldapTLS === 1) { |
|
1302 | + $this->ldap->startTls($cr); |
|
1303 | + } |
|
1304 | + |
|
1305 | + $lo = @$this->ldap->bind($cr, |
|
1306 | + $this->configuration->ldapAgentName, |
|
1307 | + $this->configuration->ldapAgentPassword); |
|
1308 | + if ($lo === true) { |
|
1309 | + $this->$cr = $cr; |
|
1310 | + return $cr; |
|
1311 | + } |
|
1312 | + |
|
1313 | + return false; |
|
1314 | + } |
|
1315 | + |
|
1316 | + /** |
|
1317 | + * @return array |
|
1318 | + */ |
|
1319 | + private function getDefaultLdapPortSettings() { |
|
1320 | + static $settings = [ |
|
1321 | + ['port' => 7636, 'tls' => false], |
|
1322 | + ['port' => 636, 'tls' => false], |
|
1323 | + ['port' => 7389, 'tls' => true], |
|
1324 | + ['port' => 389, 'tls' => true], |
|
1325 | + ['port' => 7389, 'tls' => false], |
|
1326 | + ['port' => 389, 'tls' => false], |
|
1327 | + ]; |
|
1328 | + return $settings; |
|
1329 | + } |
|
1330 | + |
|
1331 | + /** |
|
1332 | + * @return array |
|
1333 | + */ |
|
1334 | + private function getPortSettingsToTry() { |
|
1335 | + //389 ← LDAP / Unencrypted or StartTLS |
|
1336 | + //636 ← LDAPS / SSL |
|
1337 | + //7xxx ← UCS. need to be checked first, because both ports may be open |
|
1338 | + $host = $this->configuration->ldapHost; |
|
1339 | + $port = (int)$this->configuration->ldapPort; |
|
1340 | + $portSettings = []; |
|
1341 | + |
|
1342 | + //In case the port is already provided, we will check this first |
|
1343 | + if ($port > 0) { |
|
1344 | + $hostInfo = parse_url($host); |
|
1345 | + if (!(is_array($hostInfo) |
|
1346 | + && isset($hostInfo['scheme']) |
|
1347 | + && stripos($hostInfo['scheme'], 'ldaps') !== false)) { |
|
1348 | + $portSettings[] = ['port' => $port, 'tls' => true]; |
|
1349 | + } |
|
1350 | + $portSettings[] =['port' => $port, 'tls' => false]; |
|
1351 | + } |
|
1352 | + |
|
1353 | + //default ports |
|
1354 | + $portSettings = array_merge($portSettings, |
|
1355 | + $this->getDefaultLdapPortSettings()); |
|
1356 | + |
|
1357 | + return $portSettings; |
|
1358 | + } |
|
1359 | 1359 | } |
@@ -5,46 +5,46 @@ discard block |
||
5 | 5 | vendor_style('user_ldap', 'ui-multiselect/jquery.multiselect'); |
6 | 6 | |
7 | 7 | script('user_ldap', [ |
8 | - 'wizard/controller', |
|
9 | - 'wizard/configModel', |
|
10 | - 'wizard/view', |
|
11 | - 'wizard/wizardObject', |
|
12 | - 'wizard/wizardTabGeneric', |
|
13 | - 'wizard/wizardTabElementary', |
|
14 | - 'wizard/wizardTabAbstractFilter', |
|
15 | - 'wizard/wizardTabUserFilter', |
|
16 | - 'wizard/wizardTabLoginFilter', |
|
17 | - 'wizard/wizardTabGroupFilter', |
|
18 | - 'wizard/wizardTabAdvanced', |
|
19 | - 'wizard/wizardTabExpert', |
|
20 | - 'wizard/wizardDetectorQueue', |
|
21 | - 'wizard/wizardDetectorGeneric', |
|
22 | - 'wizard/wizardDetectorPort', |
|
23 | - 'wizard/wizardDetectorBaseDN', |
|
24 | - 'wizard/wizardDetectorFeatureAbstract', |
|
25 | - 'wizard/wizardDetectorUserObjectClasses', |
|
26 | - 'wizard/wizardDetectorGroupObjectClasses', |
|
27 | - 'wizard/wizardDetectorGroupsForUsers', |
|
28 | - 'wizard/wizardDetectorGroupsForGroups', |
|
29 | - 'wizard/wizardDetectorSimpleRequestAbstract', |
|
30 | - 'wizard/wizardDetectorFilterUser', |
|
31 | - 'wizard/wizardDetectorFilterLogin', |
|
32 | - 'wizard/wizardDetectorFilterGroup', |
|
33 | - 'wizard/wizardDetectorUserCount', |
|
34 | - 'wizard/wizardDetectorGroupCount', |
|
35 | - 'wizard/wizardDetectorEmailAttribute', |
|
36 | - 'wizard/wizardDetectorUserDisplayNameAttribute', |
|
37 | - 'wizard/wizardDetectorUserGroupAssociation', |
|
38 | - 'wizard/wizardDetectorAvailableAttributes', |
|
39 | - 'wizard/wizardDetectorTestAbstract', |
|
40 | - 'wizard/wizardDetectorTestLoginName', |
|
41 | - 'wizard/wizardDetectorTestBaseDN', |
|
42 | - 'wizard/wizardDetectorTestConfiguration', |
|
43 | - 'wizard/wizardDetectorClearUserMappings', |
|
44 | - 'wizard/wizardDetectorClearGroupMappings', |
|
45 | - 'wizard/wizardFilterOnType', |
|
46 | - 'wizard/wizardFilterOnTypeFactory', |
|
47 | - 'wizard/wizard' |
|
8 | + 'wizard/controller', |
|
9 | + 'wizard/configModel', |
|
10 | + 'wizard/view', |
|
11 | + 'wizard/wizardObject', |
|
12 | + 'wizard/wizardTabGeneric', |
|
13 | + 'wizard/wizardTabElementary', |
|
14 | + 'wizard/wizardTabAbstractFilter', |
|
15 | + 'wizard/wizardTabUserFilter', |
|
16 | + 'wizard/wizardTabLoginFilter', |
|
17 | + 'wizard/wizardTabGroupFilter', |
|
18 | + 'wizard/wizardTabAdvanced', |
|
19 | + 'wizard/wizardTabExpert', |
|
20 | + 'wizard/wizardDetectorQueue', |
|
21 | + 'wizard/wizardDetectorGeneric', |
|
22 | + 'wizard/wizardDetectorPort', |
|
23 | + 'wizard/wizardDetectorBaseDN', |
|
24 | + 'wizard/wizardDetectorFeatureAbstract', |
|
25 | + 'wizard/wizardDetectorUserObjectClasses', |
|
26 | + 'wizard/wizardDetectorGroupObjectClasses', |
|
27 | + 'wizard/wizardDetectorGroupsForUsers', |
|
28 | + 'wizard/wizardDetectorGroupsForGroups', |
|
29 | + 'wizard/wizardDetectorSimpleRequestAbstract', |
|
30 | + 'wizard/wizardDetectorFilterUser', |
|
31 | + 'wizard/wizardDetectorFilterLogin', |
|
32 | + 'wizard/wizardDetectorFilterGroup', |
|
33 | + 'wizard/wizardDetectorUserCount', |
|
34 | + 'wizard/wizardDetectorGroupCount', |
|
35 | + 'wizard/wizardDetectorEmailAttribute', |
|
36 | + 'wizard/wizardDetectorUserDisplayNameAttribute', |
|
37 | + 'wizard/wizardDetectorUserGroupAssociation', |
|
38 | + 'wizard/wizardDetectorAvailableAttributes', |
|
39 | + 'wizard/wizardDetectorTestAbstract', |
|
40 | + 'wizard/wizardDetectorTestLoginName', |
|
41 | + 'wizard/wizardDetectorTestBaseDN', |
|
42 | + 'wizard/wizardDetectorTestConfiguration', |
|
43 | + 'wizard/wizardDetectorClearUserMappings', |
|
44 | + 'wizard/wizardDetectorClearGroupMappings', |
|
45 | + 'wizard/wizardFilterOnType', |
|
46 | + 'wizard/wizardFilterOnTypeFactory', |
|
47 | + 'wizard/wizard' |
|
48 | 48 | ]); |
49 | 49 | |
50 | 50 | style('user_ldap', 'settings'); |
@@ -67,10 +67,10 @@ discard block |
||
67 | 67 | <li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced'));?></a></li> |
68 | 68 | </ul> |
69 | 69 | <?php |
70 | - if (!function_exists('ldap_connect')) { |
|
71 | - print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>'); |
|
72 | - } |
|
73 | - ?> |
|
70 | + if (!function_exists('ldap_connect')) { |
|
71 | + print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>'); |
|
72 | + } |
|
73 | + ?> |
|
74 | 74 | <?php require_once __DIR__ . '/part.wizard-server.php'; ?> |
75 | 75 | <?php require_once __DIR__ . '/part.wizard-userfilter.php'; ?> |
76 | 76 | <?php require_once __DIR__ . '/part.wizard-loginfilter.php'; ?> |
@@ -96,16 +96,16 @@ discard block |
||
96 | 96 | <p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree'));?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line'));?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree'));?>"></textarea></p> |
97 | 97 | <p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes'));?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes'));?>"></textarea></p> |
98 | 98 | <p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) { |
99 | - p(' selected'); |
|
100 | - } ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) { |
|
101 | - p(' selected'); |
|
102 | - } ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) { |
|
103 | - p(' selected'); |
|
104 | - } ?>>member (AD)</option><option value="gidNumber"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'gidNumber')) { |
|
105 | - p(' selected'); |
|
106 | - } ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) { |
|
107 | - p(' selected'); |
|
108 | - } ?>>zimbraMailForwardingAddress</option></select></p> |
|
99 | + p(' selected'); |
|
100 | + } ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) { |
|
101 | + p(' selected'); |
|
102 | + } ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) { |
|
103 | + p(' selected'); |
|
104 | + } ?>>member (AD)</option><option value="gidNumber"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'gidNumber')) { |
|
105 | + p(' selected'); |
|
106 | + } ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) { |
|
107 | + p(' selected'); |
|
108 | + } ?>>zimbraMailForwardingAddress</option></select></p> |
|
109 | 109 | <p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p> |
110 | 110 | <p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>" title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p> |
111 | 111 | <p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p> |
@@ -59,43 +59,43 @@ discard block |
||
59 | 59 | |
60 | 60 | <div id="ldapSettings"> |
61 | 61 | <ul> |
62 | - <li id="#ldapWizard1"><a href="#ldapWizard1"><?php p($l->t('Server'));?></a></li> |
|
63 | - <li id="#ldapWizard2"><a href="#ldapWizard2"><?php p($l->t('Users'));?></a></li> |
|
64 | - <li id="#ldapWizard3"><a href="#ldapWizard3"><?php p($l->t('Login Attributes'));?></a></li> |
|
65 | - <li id="#ldapWizard4"><a href="#ldapWizard4"><?php p($l->t('Groups'));?></a></li> |
|
66 | - <li class="ldapSettingsTabs"><a href="#ldapSettings-2"><?php p($l->t('Expert'));?></a></li> |
|
67 | - <li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced'));?></a></li> |
|
62 | + <li id="#ldapWizard1"><a href="#ldapWizard1"><?php p($l->t('Server')); ?></a></li> |
|
63 | + <li id="#ldapWizard2"><a href="#ldapWizard2"><?php p($l->t('Users')); ?></a></li> |
|
64 | + <li id="#ldapWizard3"><a href="#ldapWizard3"><?php p($l->t('Login Attributes')); ?></a></li> |
|
65 | + <li id="#ldapWizard4"><a href="#ldapWizard4"><?php p($l->t('Groups')); ?></a></li> |
|
66 | + <li class="ldapSettingsTabs"><a href="#ldapSettings-2"><?php p($l->t('Expert')); ?></a></li> |
|
67 | + <li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced')); ?></a></li> |
|
68 | 68 | </ul> |
69 | 69 | <?php |
70 | 70 | if (!function_exists('ldap_connect')) { |
71 | 71 | print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>'); |
72 | 72 | } |
73 | 73 | ?> |
74 | - <?php require_once __DIR__ . '/part.wizard-server.php'; ?> |
|
75 | - <?php require_once __DIR__ . '/part.wizard-userfilter.php'; ?> |
|
76 | - <?php require_once __DIR__ . '/part.wizard-loginfilter.php'; ?> |
|
77 | - <?php require_once __DIR__ . '/part.wizard-groupfilter.php'; ?> |
|
74 | + <?php require_once __DIR__.'/part.wizard-server.php'; ?> |
|
75 | + <?php require_once __DIR__.'/part.wizard-userfilter.php'; ?> |
|
76 | + <?php require_once __DIR__.'/part.wizard-loginfilter.php'; ?> |
|
77 | + <?php require_once __DIR__.'/part.wizard-groupfilter.php'; ?> |
|
78 | 78 | <fieldset id="ldapSettings-1"> |
79 | 79 | <div id="ldapAdvancedAccordion"> |
80 | - <h3><?php p($l->t('Connection Settings'));?></h3> |
|
80 | + <h3><?php p($l->t('Connection Settings')); ?></h3> |
|
81 | 81 | <div> |
82 | - <p><label for="ldap_configuration_active"><?php p($l->t('Configuration Active'));?></label><input type="checkbox" id="ldap_configuration_active" name="ldap_configuration_active" value="1" data-default="<?php p($_['ldap_configuration_active_default']); ?>" title="<?php p($l->t('When unchecked, this configuration will be skipped.'));?>" /></p> |
|
83 | - <p><label for="ldap_backup_host"><?php p($l->t('Backup (Replica) Host'));?></label><input type="text" id="ldap_backup_host" name="ldap_backup_host" data-default="<?php p($_['ldap_backup_host_default']); ?>" title="<?php p($l->t('Give an optional backup host. It must be a replica of the main LDAP/AD server.'));?>"></p> |
|
84 | - <p><label for="ldap_backup_port"><?php p($l->t('Backup (Replica) Port'));?></label><input type="number" id="ldap_backup_port" name="ldap_backup_port" data-default="<?php p($_['ldap_backup_port_default']); ?>" /></p> |
|
85 | - <p><label for="ldap_override_main_server"><?php p($l->t('Disable Main Server'));?></label><input type="checkbox" id="ldap_override_main_server" name="ldap_override_main_server" value="1" data-default="<?php p($_['ldap_override_main_server_default']); ?>" title="<?php p($l->t('Only connect to the replica server.'));?>" /></p> |
|
86 | - <p><label for="ldap_turn_off_cert_check"><?php p($l->t('Turn off SSL certificate validation.'));?></label><input type="checkbox" id="ldap_turn_off_cert_check" name="ldap_turn_off_cert_check" title="<?php p($l->t('Not recommended, use it for testing only! If connection only works with this option, import the LDAP server\'s SSL certificate in your %s server.', [$theme->getName()]));?>" data-default="<?php p($_['ldap_turn_off_cert_check_default']); ?>" value="1"><br/></p> |
|
87 | - <p><label for="ldap_cache_ttl"><?php p($l->t('Cache Time-To-Live'));?></label><input type="number" id="ldap_cache_ttl" name="ldap_cache_ttl" title="<?php p($l->t('in seconds. A change empties the cache.'));?>" data-default="<?php p($_['ldap_cache_ttl_default']); ?>" /></p> |
|
82 | + <p><label for="ldap_configuration_active"><?php p($l->t('Configuration Active')); ?></label><input type="checkbox" id="ldap_configuration_active" name="ldap_configuration_active" value="1" data-default="<?php p($_['ldap_configuration_active_default']); ?>" title="<?php p($l->t('When unchecked, this configuration will be skipped.')); ?>" /></p> |
|
83 | + <p><label for="ldap_backup_host"><?php p($l->t('Backup (Replica) Host')); ?></label><input type="text" id="ldap_backup_host" name="ldap_backup_host" data-default="<?php p($_['ldap_backup_host_default']); ?>" title="<?php p($l->t('Give an optional backup host. It must be a replica of the main LDAP/AD server.')); ?>"></p> |
|
84 | + <p><label for="ldap_backup_port"><?php p($l->t('Backup (Replica) Port')); ?></label><input type="number" id="ldap_backup_port" name="ldap_backup_port" data-default="<?php p($_['ldap_backup_port_default']); ?>" /></p> |
|
85 | + <p><label for="ldap_override_main_server"><?php p($l->t('Disable Main Server')); ?></label><input type="checkbox" id="ldap_override_main_server" name="ldap_override_main_server" value="1" data-default="<?php p($_['ldap_override_main_server_default']); ?>" title="<?php p($l->t('Only connect to the replica server.')); ?>" /></p> |
|
86 | + <p><label for="ldap_turn_off_cert_check"><?php p($l->t('Turn off SSL certificate validation.')); ?></label><input type="checkbox" id="ldap_turn_off_cert_check" name="ldap_turn_off_cert_check" title="<?php p($l->t('Not recommended, use it for testing only! If connection only works with this option, import the LDAP server\'s SSL certificate in your %s server.', [$theme->getName()])); ?>" data-default="<?php p($_['ldap_turn_off_cert_check_default']); ?>" value="1"><br/></p> |
|
87 | + <p><label for="ldap_cache_ttl"><?php p($l->t('Cache Time-To-Live')); ?></label><input type="number" id="ldap_cache_ttl" name="ldap_cache_ttl" title="<?php p($l->t('in seconds. A change empties the cache.')); ?>" data-default="<?php p($_['ldap_cache_ttl_default']); ?>" /></p> |
|
88 | 88 | </div> |
89 | - <h3><?php p($l->t('Directory Settings'));?></h3> |
|
89 | + <h3><?php p($l->t('Directory Settings')); ?></h3> |
|
90 | 90 | <div> |
91 | - <p><label for="ldap_display_name"><?php p($l->t('User Display Name Field'));?></label><input type="text" id="ldap_display_name" name="ldap_display_name" data-default="<?php p($_['ldap_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the user\'s display name.'));?>" /></p> |
|
92 | - <p><label for="ldap_user_display_name_2"><?php p($l->t('2nd User Display Name Field'));?></label><input type="text" id="ldap_user_display_name_2" name="ldap_user_display_name_2" data-default="<?php p($_['ldap_user_display_name_2_default']); ?>" title="<?php p($l->t('Optional. An LDAP attribute to be added to the display name in brackets. Results in e.g. »John Doe ([email protected])«.'));?>" /></p> |
|
93 | - <p><label for="ldap_base_users"><?php p($l->t('Base User Tree'));?></label><textarea id="ldap_base_users" name="ldap_base_users" placeholder="<?php p($l->t('One User Base DN per line'));?>" data-default="<?php p($_['ldap_base_users_default']); ?>" title="<?php p($l->t('Base User Tree'));?>"></textarea></p> |
|
94 | - <p><label for="ldap_attributes_for_user_search"><?php p($l->t('User Search Attributes'));?></label><textarea id="ldap_attributes_for_user_search" name="ldap_attributes_for_user_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_user_search_default']); ?>" title="<?php p($l->t('User Search Attributes'));?>"></textarea></p> |
|
95 | - <p><label for="ldap_group_display_name"><?php p($l->t('Group Display Name Field'));?></label><input type="text" id="ldap_group_display_name" name="ldap_group_display_name" data-default="<?php p($_['ldap_group_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the groups\'s display name.'));?>" /></p> |
|
96 | - <p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree'));?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line'));?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree'));?>"></textarea></p> |
|
97 | - <p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes'));?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes'));?>"></textarea></p> |
|
98 | - <p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) { |
|
91 | + <p><label for="ldap_display_name"><?php p($l->t('User Display Name Field')); ?></label><input type="text" id="ldap_display_name" name="ldap_display_name" data-default="<?php p($_['ldap_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the user\'s display name.')); ?>" /></p> |
|
92 | + <p><label for="ldap_user_display_name_2"><?php p($l->t('2nd User Display Name Field')); ?></label><input type="text" id="ldap_user_display_name_2" name="ldap_user_display_name_2" data-default="<?php p($_['ldap_user_display_name_2_default']); ?>" title="<?php p($l->t('Optional. An LDAP attribute to be added to the display name in brackets. Results in e.g. »John Doe ([email protected])«.')); ?>" /></p> |
|
93 | + <p><label for="ldap_base_users"><?php p($l->t('Base User Tree')); ?></label><textarea id="ldap_base_users" name="ldap_base_users" placeholder="<?php p($l->t('One User Base DN per line')); ?>" data-default="<?php p($_['ldap_base_users_default']); ?>" title="<?php p($l->t('Base User Tree')); ?>"></textarea></p> |
|
94 | + <p><label for="ldap_attributes_for_user_search"><?php p($l->t('User Search Attributes')); ?></label><textarea id="ldap_attributes_for_user_search" name="ldap_attributes_for_user_search" placeholder="<?php p($l->t('Optional; one attribute per line')); ?>" data-default="<?php p($_['ldap_attributes_for_user_search_default']); ?>" title="<?php p($l->t('User Search Attributes')); ?>"></textarea></p> |
|
95 | + <p><label for="ldap_group_display_name"><?php p($l->t('Group Display Name Field')); ?></label><input type="text" id="ldap_group_display_name" name="ldap_group_display_name" data-default="<?php p($_['ldap_group_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the groups\'s display name.')); ?>" /></p> |
|
96 | + <p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree')); ?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line')); ?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree')); ?>"></textarea></p> |
|
97 | + <p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes')); ?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line')); ?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes')); ?>"></textarea></p> |
|
98 | + <p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association')); ?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) { |
|
99 | 99 | p(' selected'); |
100 | 100 | } ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) { |
101 | 101 | p(' selected'); |
@@ -106,35 +106,35 @@ discard block |
||
106 | 106 | } ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) { |
107 | 107 | p(' selected'); |
108 | 108 | } ?>>zimbraMailForwardingAddress</option></select></p> |
109 | - <p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p> |
|
110 | - <p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>" title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p> |
|
111 | - <p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p> |
|
112 | - <p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user'));?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.'));?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)'));?></span></span> |
|
109 | + <p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL')); ?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)')); ?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p> |
|
110 | + <p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups')); ?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>" title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)')); ?>" /></p> |
|
111 | + <p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize')); ?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)')); ?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p> |
|
112 | + <p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user')); ?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.')); ?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)')); ?></span></span> |
|
113 | 113 | </span><br/></p> |
114 | - <p><label for="ldap_default_ppolicy_dn"><?php p($l->t('Default password policy DN'));?></label><input type="text" id="ldap_default_ppolicy_dn" name="ldap_default_ppolicy_dn" title="<?php p($l->t('The DN of a default password policy that will be used for password expiry handling. Works only when LDAP password changes per user are enabled and is only supported by OpenLDAP. Leave empty to disable password expiry handling.'));?>" data-default="<?php p($_['ldap_default_ppolicy_dn_default']); ?>" /></p> |
|
114 | + <p><label for="ldap_default_ppolicy_dn"><?php p($l->t('Default password policy DN')); ?></label><input type="text" id="ldap_default_ppolicy_dn" name="ldap_default_ppolicy_dn" title="<?php p($l->t('The DN of a default password policy that will be used for password expiry handling. Works only when LDAP password changes per user are enabled and is only supported by OpenLDAP. Leave empty to disable password expiry handling.')); ?>" data-default="<?php p($_['ldap_default_ppolicy_dn_default']); ?>" /></p> |
|
115 | 115 | </div> |
116 | - <h3><?php p($l->t('Special Attributes'));?></h3> |
|
116 | + <h3><?php p($l->t('Special Attributes')); ?></h3> |
|
117 | 117 | <div> |
118 | - <p><label for="ldap_quota_attr"><?php p($l->t('Quota Field'));?></label><input type="text" id="ldap_quota_attr" name="ldap_quota_attr" data-default="<?php p($_['ldap_quota_attr_default']); ?>" title="<?php p($l->t('Leave empty for user\'s default quota. Otherwise, specify an LDAP/AD attribute.'));?>" /></p> |
|
119 | - <p><label for="ldap_quota_def"><?php p($l->t('Quota Default'));?></label><input type="text" id="ldap_quota_def" name="ldap_quota_def" data-default="<?php p($_['ldap_quota_def_default']); ?>" title="<?php p($l->t('Override default quota for LDAP users who do not have a quota set in the Quota Field.'));?>" /></p> |
|
120 | - <p><label for="ldap_email_attr"><?php p($l->t('Email Field'));?></label><input type="text" id="ldap_email_attr" name="ldap_email_attr" data-default="<?php p($_['ldap_email_attr_default']); ?>" title="<?php p($l->t('Set the user\'s email from their LDAP attribute. Leave it empty for default behaviour.'));?>" /></p> |
|
121 | - <p><label for="home_folder_naming_rule"><?php p($l->t('User Home Folder Naming Rule'));?></label><input type="text" id="home_folder_naming_rule" name="home_folder_naming_rule" title="<?php p($l->t('Leave empty for username (default). Otherwise, specify an LDAP/AD attribute.'));?>" data-default="<?php p($_['home_folder_naming_rule_default']); ?>" /></p> |
|
118 | + <p><label for="ldap_quota_attr"><?php p($l->t('Quota Field')); ?></label><input type="text" id="ldap_quota_attr" name="ldap_quota_attr" data-default="<?php p($_['ldap_quota_attr_default']); ?>" title="<?php p($l->t('Leave empty for user\'s default quota. Otherwise, specify an LDAP/AD attribute.')); ?>" /></p> |
|
119 | + <p><label for="ldap_quota_def"><?php p($l->t('Quota Default')); ?></label><input type="text" id="ldap_quota_def" name="ldap_quota_def" data-default="<?php p($_['ldap_quota_def_default']); ?>" title="<?php p($l->t('Override default quota for LDAP users who do not have a quota set in the Quota Field.')); ?>" /></p> |
|
120 | + <p><label for="ldap_email_attr"><?php p($l->t('Email Field')); ?></label><input type="text" id="ldap_email_attr" name="ldap_email_attr" data-default="<?php p($_['ldap_email_attr_default']); ?>" title="<?php p($l->t('Set the user\'s email from their LDAP attribute. Leave it empty for default behaviour.')); ?>" /></p> |
|
121 | + <p><label for="home_folder_naming_rule"><?php p($l->t('User Home Folder Naming Rule')); ?></label><input type="text" id="home_folder_naming_rule" name="home_folder_naming_rule" title="<?php p($l->t('Leave empty for username (default). Otherwise, specify an LDAP/AD attribute.')); ?>" data-default="<?php p($_['home_folder_naming_rule_default']); ?>" /></p> |
|
122 | 122 | <p><label for="ldap_ext_storage_home_attribute"> <?php p($l->t('"$home" Placeholder Field')); ?></label><input type="text" id="ldap_ext_storage_home_attribute" name="ldap_ext_storage_home_attribute" title="<?php p($l->t('$home in an external storage configuration will be replaced with the value of the specified attribute')); ?>" data-default="<?php p($_['ldap_ext_storage_home_attribute_default']); ?>"></p> |
123 | 123 | </div> |
124 | 124 | </div> |
125 | 125 | <?php print_unescaped($_['settingControls']); ?> |
126 | 126 | </fieldset> |
127 | 127 | <fieldset id="ldapSettings-2"> |
128 | - <p><strong><?php p($l->t('Internal Username'));?></strong></p> |
|
129 | - <p class="ldapIndent"><?php p($l->t('By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ]. Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users.'));?></p> |
|
130 | - <p class="ldapIndent"><label for="ldap_expert_username_attr"><?php p($l->t('Internal Username Attribute:'));?></label><input type="text" id="ldap_expert_username_attr" name="ldap_expert_username_attr" data-default="<?php p($_['ldap_expert_username_attr_default']); ?>" /></p> |
|
131 | - <p><strong><?php p($l->t('Override UUID detection'));?></strong></p> |
|
132 | - <p class="ldapIndent"><?php p($l->t('By default, the UUID attribute is automatically detected. The UUID attribute is used to doubtlessly identify LDAP users and groups. Also, the internal username will be created based on the UUID, if not specified otherwise above. You can override the setting and pass an attribute of your choice. You must make sure that the attribute of your choice can be fetched for both users and groups and it is unique. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users and groups.'));?></p> |
|
133 | - <p class="ldapIndent"><label for="ldap_expert_uuid_user_attr"><?php p($l->t('UUID Attribute for Users:'));?></label><input type="text" id="ldap_expert_uuid_user_attr" name="ldap_expert_uuid_user_attr" data-default="<?php p($_['ldap_expert_uuid_user_attr_default']); ?>" /></p> |
|
134 | - <p class="ldapIndent"><label for="ldap_expert_uuid_group_attr"><?php p($l->t('UUID Attribute for Groups:'));?></label><input type="text" id="ldap_expert_uuid_group_attr" name="ldap_expert_uuid_group_attr" data-default="<?php p($_['ldap_expert_uuid_group_attr_default']); ?>" /></p> |
|
135 | - <p><strong><?php p($l->t('Username-LDAP User Mapping'));?></strong></p> |
|
136 | - <p class="ldapIndent"><?php p($l->t('Usernames are used to store and assign metadata. In order to precisely identify and recognize users, each LDAP user will have an internal username. This requires a mapping from username to LDAP user. The created username is mapped to the UUID of the LDAP user. Additionally the DN is cached as well to reduce LDAP interaction, but it is not used for identification. If the DN changes, the changes will be found. The internal username is used all over. Clearing the mappings will have leftovers everywhere. Clearing the mappings is not configuration sensitive, it affects all LDAP configurations! Never clear the mappings in a production environment, only in a testing or experimental stage.'));?></p> |
|
137 | - <p class="ldapIndent"><button type="button" id="ldap_action_clear_user_mappings" name="ldap_action_clear_user_mappings"><?php p($l->t('Clear Username-LDAP User Mapping'));?></button><br/><button type="button" id="ldap_action_clear_group_mappings" name="ldap_action_clear_group_mappings"><?php p($l->t('Clear Groupname-LDAP Group Mapping'));?></button></p> |
|
128 | + <p><strong><?php p($l->t('Internal Username')); ?></strong></p> |
|
129 | + <p class="ldapIndent"><?php p($l->t('By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ]. Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users.')); ?></p> |
|
130 | + <p class="ldapIndent"><label for="ldap_expert_username_attr"><?php p($l->t('Internal Username Attribute:')); ?></label><input type="text" id="ldap_expert_username_attr" name="ldap_expert_username_attr" data-default="<?php p($_['ldap_expert_username_attr_default']); ?>" /></p> |
|
131 | + <p><strong><?php p($l->t('Override UUID detection')); ?></strong></p> |
|
132 | + <p class="ldapIndent"><?php p($l->t('By default, the UUID attribute is automatically detected. The UUID attribute is used to doubtlessly identify LDAP users and groups. Also, the internal username will be created based on the UUID, if not specified otherwise above. You can override the setting and pass an attribute of your choice. You must make sure that the attribute of your choice can be fetched for both users and groups and it is unique. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users and groups.')); ?></p> |
|
133 | + <p class="ldapIndent"><label for="ldap_expert_uuid_user_attr"><?php p($l->t('UUID Attribute for Users:')); ?></label><input type="text" id="ldap_expert_uuid_user_attr" name="ldap_expert_uuid_user_attr" data-default="<?php p($_['ldap_expert_uuid_user_attr_default']); ?>" /></p> |
|
134 | + <p class="ldapIndent"><label for="ldap_expert_uuid_group_attr"><?php p($l->t('UUID Attribute for Groups:')); ?></label><input type="text" id="ldap_expert_uuid_group_attr" name="ldap_expert_uuid_group_attr" data-default="<?php p($_['ldap_expert_uuid_group_attr_default']); ?>" /></p> |
|
135 | + <p><strong><?php p($l->t('Username-LDAP User Mapping')); ?></strong></p> |
|
136 | + <p class="ldapIndent"><?php p($l->t('Usernames are used to store and assign metadata. In order to precisely identify and recognize users, each LDAP user will have an internal username. This requires a mapping from username to LDAP user. The created username is mapped to the UUID of the LDAP user. Additionally the DN is cached as well to reduce LDAP interaction, but it is not used for identification. If the DN changes, the changes will be found. The internal username is used all over. Clearing the mappings will have leftovers everywhere. Clearing the mappings is not configuration sensitive, it affects all LDAP configurations! Never clear the mappings in a production environment, only in a testing or experimental stage.')); ?></p> |
|
137 | + <p class="ldapIndent"><button type="button" id="ldap_action_clear_user_mappings" name="ldap_action_clear_user_mappings"><?php p($l->t('Clear Username-LDAP User Mapping')); ?></button><br/><button type="button" id="ldap_action_clear_group_mappings" name="ldap_action_clear_group_mappings"><?php p($l->t('Clear Groupname-LDAP Group Mapping')); ?></button></p> |
|
138 | 138 | <?php print_unescaped($_['settingControls']); ?> |
139 | 139 | </fieldset> |
140 | 140 | </div> |