Passed
Push — master ( 138f47...88fcc0 )
by Blizzz
10:57 queued 11s
created

Group_LDAP::getUserGidNumber()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Alexander Bergolth <[email protected]>
6
 * @author Alex Weirig <[email protected]>
7
 * @author alexweirig <[email protected]>
8
 * @author Andreas Fischer <[email protected]>
9
 * @author Andreas Pflug <[email protected]>
10
 * @author Arthur Schiwon <[email protected]>
11
 * @author Bart Visscher <[email protected]>
12
 * @author Christoph Wurst <[email protected]>
13
 * @author Clement Wong <[email protected]>
14
 * @author Frédéric Fortier <[email protected]>
15
 * @author Joas Schilling <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Morris Jobke <[email protected]>
18
 * @author Nicolas Grekas <[email protected]>
19
 * @author Robin McCorkell <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 * @author Roland Tapken <[email protected]>
22
 * @author Thomas Müller <[email protected]>
23
 * @author Tobias Perschon <[email protected]>
24
 * @author Victor Dubiniuk <[email protected]>
25
 * @author Vincent Petry <[email protected]>
26
 * @author Vinicius Cubas Brand <[email protected]>
27
 * @author Xuanwo <[email protected]>
28
 *
29
 * @license AGPL-3.0
30
 *
31
 * This code is free software: you can redistribute it and/or modify
32
 * it under the terms of the GNU Affero General Public License, version 3,
33
 * as published by the Free Software Foundation.
34
 *
35
 * This program is distributed in the hope that it will be useful,
36
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
37
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38
 * GNU Affero General Public License for more details.
39
 *
40
 * You should have received a copy of the GNU Affero General Public License, version 3,
41
 * along with this program. If not, see <http://www.gnu.org/licenses/>
42
 *
43
 */
44
45
namespace OCA\User_LDAP;
46
47
use Closure;
48
use Exception;
49
use OC;
50
use OC\Cache\CappedMemoryCache;
51
use OC\ServerNotAvailableException;
52
use OCP\Group\Backend\IGetDisplayNameBackend;
53
use OCP\GroupInterface;
54
use OCP\ILogger;
55
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();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedGroupMembers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
84
		$this->cachedGroupsByMember = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedGroupsByMember.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
85
		$this->cachedNestedGroups = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedNestedGroups.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
86
		$this->groupPluginManager = $groupPluginManager;
87
		$this->logger = OC::$server->getLogger();
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getLogger() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

87
		$this->logger = /** @scrutinizer ignore-deprecated */ OC::$server->getLogger();
Loading history...
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]);
0 ignored issues
show
Bug introduced by
$this->cachedGroupMembers[$gid] of type string is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
			return in_array($userDN, /** @scrutinizer ignore-type */ $this->cachedGroupMembers[$gid]);
Loading history...
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) {
0 ignored issues
show
introduced by
The condition is_array($members) is always true.
Loading history...
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');
0 ignored issues
show
Bug introduced by
'dn' of type string is incompatible with the type array|null expected by parameter $attr of OCA\User_LDAP\Access::searchUsers(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
				$foundMembers = $this->access->searchUsers($memberUrlFilter, /** @scrutinizer ignore-type */ 'dn');
Loading history...
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
250
		if ($this->access->connection->ldapNestedGroups
251
			&& $this->access->connection->useMemberOfToDetectMembership
252
			&& $this->access->connection->hasMemberOfFilterSupport
253
			&& $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
254
		) {
255
			$attemptedLdapMatchingRuleInChain = true;
256
			// compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
257
			$filter = $this->access->combineFilterWithAnd([
258
				$this->access->connection->ldapUserFilter,
259
				$this->access->connection->ldapUserDisplayName . '=*',
260
				'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
261
			]);
262
			$memberRecords = $this->access->fetchListOfUsers(
263
				$filter,
264
				$this->access->userManager->getAttributes(true)
265
			);
266
			$result = array_reduce($memberRecords, function ($carry, $record) {
267
				$carry[] = $record['dn'][0];
268
				return $carry;
269
			}, []);
270
			if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
271
				return $result;
272
			} elseif (!empty($memberRecords)) {
273
				$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
274
				$this->access->connection->saveConfiguration();
275
				return $result;
276
			}
277
			// when feature availability is unknown, and the result is empty, continue and test with original approach
278
		}
279
280
		$seen[$dnGroup] = 1;
281
		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
282
		if (is_array($members)) {
283
			$fetcher = function ($memberDN, &$seen) {
284
				return $this->_groupMembers($memberDN, $seen);
285
			};
286
			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
287
		}
288
289
		$allMembers += $this->getDynamicGroupMembers($dnGroup);
290
291
		$this->access->connection->writeToCache($cacheKey, $allMembers);
292
		if (isset($attemptedLdapMatchingRuleInChain)
293
			&& $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
294
			&& !empty($allMembers)
295
		) {
296
			$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
297
			$this->access->connection->saveConfiguration();
298
		}
299
		return $allMembers;
300
	}
301
302
	/**
303
	 * @throws ServerNotAvailableException
304
	 */
305
	private function _getGroupDNsFromMemberOf(string $dn): array {
306
		$groups = $this->access->readAttribute($dn, 'memberOf');
307
		if (!is_array($groups)) {
308
			return [];
309
		}
310
311
		$fetcher = function ($groupDN) {
312
			if (isset($this->cachedNestedGroups[$groupDN])) {
313
				$nestedGroups = $this->cachedNestedGroups[$groupDN];
314
			} else {
315
				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
316
				if (!is_array($nestedGroups)) {
317
					$nestedGroups = [];
318
				}
319
				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
320
			}
321
			return $nestedGroups;
322
		};
323
324
		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
325
		return $this->filterValidGroups($groups);
326
	}
327
328
	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
329
		$nesting = (int)$this->access->connection->ldapNestedGroups;
330
		// depending on the input, we either have a list of DNs or a list of LDAP records
331
		// also, the output expects either DNs or records. Testing the first element should suffice.
332
		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
333
334
		if ($nesting !== 1) {
335
			if ($recordMode) {
336
				// the keys are numeric, but should hold the DN
337
				return array_reduce($list, function ($transformed, $record) use ($dn) {
338
					if ($record['dn'][0] != $dn) {
339
						$transformed[$record['dn'][0]] = $record;
340
					}
341
					return $transformed;
342
				}, []);
343
			}
344
			return $list;
345
		}
346
347
		$seen = [];
348
		while ($record = array_pop($list)) {
349
			$recordDN = $recordMode ? $record['dn'][0] : $record;
350
			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
351
				// Prevent loops
352
				continue;
353
			}
354
			$fetched = $fetcher($record, $seen);
355
			$list = array_merge($list, $fetched);
356
			$seen[$recordDN] = $record;
357
		}
358
359
		return $recordMode ? $seen : array_keys($seen);
360
	}
361
362
	/**
363
	 * translates a gidNumber into an ownCloud internal name
364
	 *
365
	 * @return string|bool
366
	 * @throws Exception
367
	 * @throws ServerNotAvailableException
368
	 */
369
	public function gidNumber2Name(string $gid, string $dn) {
0 ignored issues
show
Unused Code introduced by
The parameter $dn is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

369
	public function gidNumber2Name(string $gid, /** @scrutinizer ignore-unused */ string $dn) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
370
		$cacheKey = 'gidNumberToName' . $gid;
371
		$groupName = $this->access->connection->getFromCache($cacheKey);
372
		if (!is_null($groupName) && isset($groupName)) {
373
			return $groupName;
374
		}
375
376
		//we need to get the DN from LDAP
377
		$filter = $this->access->combineFilterWithAnd([
378
			$this->access->connection->ldapGroupFilter,
379
			'objectClass=posixGroup',
380
			$this->access->connection->ldapGidNumber . '=' . $gid
381
		]);
382
		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
383
	}
384
385
	/**
386
	 * @throws ServerNotAvailableException
387
	 * @throws Exception
388
	 */
389
	private function getNameOfGroup(string $filter, string $cacheKey) {
390
		$result = $this->access->searchGroups($filter, ['dn'], 1);
391
		if (empty($result)) {
392
			return null;
393
		}
394
		$dn = $result[0]['dn'][0];
395
396
		//and now the group name
397
		//NOTE once we have separate Nextcloud group IDs and group names we can
398
		//directly read the display name attribute instead of the DN
399
		$name = $this->access->dn2groupname($dn);
400
401
		$this->access->connection->writeToCache($cacheKey, $name);
402
403
		return $name;
404
	}
405
406
	/**
407
	 * returns the entry's gidNumber
408
	 *
409
	 * @return string|bool
410
	 * @throws ServerNotAvailableException
411
	 */
412
	private function getEntryGidNumber(string $dn, string $attribute) {
413
		$value = $this->access->readAttribute($dn, $attribute);
414
		if (is_array($value) && !empty($value)) {
415
			return $value[0];
416
		}
417
		return false;
418
	}
419
420
	/**
421
	 * @return string|bool
422
	 * @throws ServerNotAvailableException
423
	 */
424
	public function getGroupGidNumber(string $dn) {
425
		return $this->getEntryGidNumber($dn, 'gidNumber');
426
	}
427
428
	/**
429
	 * returns the user's gidNumber
430
	 *
431
	 * @return string|bool
432
	 * @throws ServerNotAvailableException
433
	 */
434
	public function getUserGidNumber(string $dn) {
435
		$gidNumber = false;
436
		if ($this->access->connection->hasGidNumber) {
437
			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
438
			if ($gidNumber === false) {
439
				$this->access->connection->hasGidNumber = false;
440
			}
441
		}
442
		return $gidNumber;
443
	}
444
445
	/**
446
	 * @throws ServerNotAvailableException
447
	 * @throws Exception
448
	 */
449
	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
450
		$groupID = $this->getGroupGidNumber($groupDN);
451
		if ($groupID === false) {
452
			throw new Exception('Not a valid group');
453
		}
454
455
		$filterParts = [];
456
		$filterParts[] = $this->access->getFilterForUserCount();
457
		if ($search !== '') {
458
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
459
		}
460
		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
0 ignored issues
show
Bug introduced by
Are you sure $groupID of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

460
		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
461
462
		return $this->access->combineFilterWithAnd($filterParts);
463
	}
464
465
	/**
466
	 * returns a list of users that have the given group as gid number
467
	 *
468
	 * @throws ServerNotAvailableException
469
	 */
470
	public function getUsersInGidNumber(
471
		string $groupDN,
472
		string $search = '',
473
		?int $limit = -1,
474
		?int $offset = 0
475
	): array {
476
		try {
477
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
478
			$users = $this->access->fetchListOfUsers(
479
				$filter,
480
				[$this->access->connection->ldapUserDisplayName, 'dn'],
481
				$limit,
482
				$offset
483
			);
484
			return $this->access->nextcloudUserNames($users);
485
		} catch (ServerNotAvailableException $e) {
486
			throw $e;
487
		} catch (Exception $e) {
488
			return [];
489
		}
490
	}
491
492
	/**
493
	 * @throws ServerNotAvailableException
494
	 * @return bool
495
	 */
496
	public function getUserGroupByGid(string $dn) {
497
		$groupID = $this->getUserGidNumber($dn);
498
		if ($groupID !== false) {
499
			$groupName = $this->gidNumber2Name($groupID, $dn);
500
			if ($groupName !== false) {
501
				return $groupName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $groupName also could return the type string which is incompatible with the documented return type boolean.
Loading history...
502
			}
503
		}
504
505
		return false;
506
	}
507
508
	/**
509
	 * translates a primary group ID into an Nextcloud internal name
510
	 *
511
	 * @return string|bool
512
	 * @throws Exception
513
	 * @throws ServerNotAvailableException
514
	 */
515
	public function primaryGroupID2Name(string $gid, string $dn) {
516
		$cacheKey = 'primaryGroupIDtoName';
517
		$groupNames = $this->access->connection->getFromCache($cacheKey);
518
		if (!is_null($groupNames) && isset($groupNames[$gid])) {
519
			return $groupNames[$gid];
520
		}
521
522
		$domainObjectSid = $this->access->getSID($dn);
523
		if ($domainObjectSid === false) {
524
			return false;
525
		}
526
527
		//we need to get the DN from LDAP
528
		$filter = $this->access->combineFilterWithAnd([
529
			$this->access->connection->ldapGroupFilter,
530
			'objectsid=' . $domainObjectSid . '-' . $gid
0 ignored issues
show
Bug introduced by
Are you sure $domainObjectSid of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

530
			'objectsid=' . /** @scrutinizer ignore-type */ $domainObjectSid . '-' . $gid
Loading history...
531
		]);
532
		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
533
	}
534
535
	/**
536
	 * returns the entry's primary group ID
537
	 *
538
	 * @return string|bool
539
	 * @throws ServerNotAvailableException
540
	 */
541
	private function getEntryGroupID(string $dn, string $attribute) {
542
		$value = $this->access->readAttribute($dn, $attribute);
543
		if (is_array($value) && !empty($value)) {
544
			return $value[0];
545
		}
546
		return false;
547
	}
548
549
	/**
550
	 * @return string|bool
551
	 * @throws ServerNotAvailableException
552
	 */
553
	public function getGroupPrimaryGroupID(string $dn) {
554
		return $this->getEntryGroupID($dn, 'primaryGroupToken');
555
	}
556
557
	/**
558
	 * @return string|bool
559
	 * @throws ServerNotAvailableException
560
	 */
561
	public function getUserPrimaryGroupIDs(string $dn) {
562
		$primaryGroupID = false;
563
		if ($this->access->connection->hasPrimaryGroups) {
564
			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
565
			if ($primaryGroupID === false) {
566
				$this->access->connection->hasPrimaryGroups = false;
567
			}
568
		}
569
		return $primaryGroupID;
570
	}
571
572
	/**
573
	 * @throws Exception
574
	 * @throws ServerNotAvailableException
575
	 */
576
	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
577
		$groupID = $this->getGroupPrimaryGroupID($groupDN);
578
		if ($groupID === false) {
579
			throw new Exception('Not a valid group');
580
		}
581
582
		$filterParts = [];
583
		$filterParts[] = $this->access->getFilterForUserCount();
584
		if ($search !== '') {
585
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
586
		}
587
		$filterParts[] = 'primaryGroupID=' . $groupID;
0 ignored issues
show
Bug introduced by
Are you sure $groupID of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

587
		$filterParts[] = 'primaryGroupID=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
588
589
		return $this->access->combineFilterWithAnd($filterParts);
590
	}
591
592
	/**
593
	 * @throws ServerNotAvailableException
594
	 */
595
	public function getUsersInPrimaryGroup(
596
		string $groupDN,
597
		string $search = '',
598
		?int $limit = -1,
599
		?int $offset = 0
600
	): array {
601
		try {
602
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
603
			$users = $this->access->fetchListOfUsers(
604
				$filter,
605
				[$this->access->connection->ldapUserDisplayName, 'dn'],
606
				$limit,
607
				$offset
608
			);
609
			return $this->access->nextcloudUserNames($users);
610
		} catch (ServerNotAvailableException $e) {
611
			throw $e;
612
		} catch (Exception $e) {
613
			return [];
614
		}
615
	}
616
617
	/**
618
	 * @throws ServerNotAvailableException
619
	 */
620
	public function countUsersInPrimaryGroup(
621
		string $groupDN,
622
		string $search = '',
623
		int $limit = -1,
624
		int $offset = 0
625
	): int {
626
		try {
627
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
628
			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
629
			return (int)$users;
630
		} catch (ServerNotAvailableException $e) {
631
			throw $e;
632
		} catch (Exception $e) {
633
			return 0;
634
		}
635
	}
636
637
	/**
638
	 * @return string|bool
639
	 * @throws ServerNotAvailableException
640
	 */
641
	public function getUserPrimaryGroup(string $dn) {
642
		$groupID = $this->getUserPrimaryGroupIDs($dn);
643
		if ($groupID !== false) {
644
			$groupName = $this->primaryGroupID2Name($groupID, $dn);
645
			if ($groupName !== false) {
646
				return $groupName;
647
			}
648
		}
649
650
		return false;
651
	}
652
653
	/**
654
	 * This function fetches all groups a user belongs to. It does not check
655
	 * if the user exists at all.
656
	 *
657
	 * This function includes groups based on dynamic group membership.
658
	 *
659
	 * @param string $uid Name of the user
660
	 * @return array with group names
661
	 * @throws Exception
662
	 * @throws ServerNotAvailableException
663
	 */
664
	public function getUserGroups($uid) {
665
		if (!$this->enabled) {
666
			return [];
667
		}
668
		$cacheKey = 'getUserGroups' . $uid;
669
		$userGroups = $this->access->connection->getFromCache($cacheKey);
670
		if (!is_null($userGroups)) {
671
			return $userGroups;
672
		}
673
		$userDN = $this->access->username2dn($uid);
674
		if (!$userDN) {
675
			$this->access->connection->writeToCache($cacheKey, []);
676
			return [];
677
		}
678
679
		$groups = [];
680
		$primaryGroup = $this->getUserPrimaryGroup($userDN);
681
		$gidGroupName = $this->getUserGroupByGid($userDN);
682
683
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
684
685
		if (!empty($dynamicGroupMemberURL)) {
686
			// look through dynamic groups to add them to the result array if needed
687
			$groupsToMatch = $this->access->fetchListOfGroups(
688
				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
689
			foreach ($groupsToMatch as $dynamicGroup) {
690
				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
691
					continue;
692
				}
693
				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
694
				if ($pos !== false) {
695
					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
696
					// apply filter via ldap search to see if this user is in this
697
					// dynamic group
698
					$userMatch = $this->access->readAttribute(
699
						$userDN,
700
						$this->access->connection->ldapUserDisplayName,
701
						$memberUrlFilter
702
					);
703
					if ($userMatch !== false) {
704
						// match found so this user is in this group
705
						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
706
						if (is_string($groupName)) {
707
							// be sure to never return false if the dn could not be
708
							// resolved to a name, for whatever reason.
709
							$groups[] = $groupName;
710
						}
711
					}
712
				} else {
713
					$this->logger->debug('No search filter found on member url of group {dn}',
714
						[
715
							'app' => 'user_ldap',
716
							'dn' => $dynamicGroup,
717
						]
718
					);
719
				}
720
			}
721
		}
722
723
		// if possible, read out membership via memberOf. It's far faster than
724
		// performing a search, which still is a fallback later.
725
		// memberof doesn't support memberuid, so skip it here.
726
		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
727
			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
728
			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
729
			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
730
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
731
			if (is_array($groupDNs)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
732
				foreach ($groupDNs as $dn) {
733
					$groupName = $this->access->dn2groupname($dn);
734
					if (is_string($groupName)) {
735
						// be sure to never return false if the dn could not be
736
						// resolved to a name, for whatever reason.
737
						$groups[] = $groupName;
738
					}
739
				}
740
			}
741
742
			if ($primaryGroup !== false) {
743
				$groups[] = $primaryGroup;
744
			}
745
			if ($gidGroupName !== false) {
746
				$groups[] = $gidGroupName;
747
			}
748
			$this->access->connection->writeToCache($cacheKey, $groups);
749
			return $groups;
750
		}
751
752
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
753
		switch ($this->ldapGroupMemberAssocAttr) {
754
			case 'uniquemember':
755
			case 'member':
756
				$uid = $userDN;
757
				break;
758
759
			case 'memberuid':
760
			case 'zimbramailforwardingaddress':
761
				$result = $this->access->readAttribute($userDN, 'uid');
762
				if ($result === false) {
763
					$this->logger->debug('No uid attribute found for DN {dn} on {host}',
764
						[
765
							'app' => 'user_ldap',
766
							'dn' => $userDN,
767
							'host' => $this->access->connection->ldapHost,
768
						]
769
					);
770
					$uid = false;
771
				} else {
772
					$uid = $result[0];
773
				}
774
				break;
775
776
			default:
777
				// just in case
778
				$uid = $userDN;
779
				break;
780
		}
781
782
		if ($uid !== false) {
783
			if (isset($this->cachedGroupsByMember[$uid])) {
784
				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
0 ignored issues
show
Bug introduced by
$this->cachedGroupsByMember[$uid] of type string is incompatible with the type array|null expected by parameter $array2 of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

784
				$groups = array_merge($groups, /** @scrutinizer ignore-type */ $this->cachedGroupsByMember[$uid]);
Loading history...
785
			} else {
786
				$groupsByMember = array_values($this->getGroupsByMember($uid));
787
				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
788
				$this->cachedGroupsByMember[$uid] = $groupsByMember;
789
				$groups = array_merge($groups, $groupsByMember);
790
			}
791
		}
792
793
		if ($primaryGroup !== false) {
794
			$groups[] = $primaryGroup;
795
		}
796
		if ($gidGroupName !== false) {
797
			$groups[] = $gidGroupName;
798
		}
799
800
		$groups = array_unique($groups, SORT_LOCALE_STRING);
801
		$this->access->connection->writeToCache($cacheKey, $groups);
802
803
		return $groups;
804
	}
805
806
	/**
807
	 * @throws ServerNotAvailableException
808
	 */
809
	private function getGroupsByMember(string $dn, array &$seen = null): array {
810
		if ($seen === null) {
811
			$seen = [];
812
		}
813
		if (array_key_exists($dn, $seen)) {
814
			// avoid loops
815
			return [];
816
		}
817
		$allGroups = [];
818
		$seen[$dn] = true;
819
		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
820
821
		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
822
			//in this case the member entries are email addresses
823
			$filter .= '@*';
824
		}
825
826
		$groups = $this->access->fetchListOfGroups($filter,
827
			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
828
		if (is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
829
			$fetcher = function ($dn, &$seen) {
830
				if (is_array($dn) && isset($dn['dn'][0])) {
831
					$dn = $dn['dn'][0];
832
				}
833
				return $this->getGroupsByMember($dn, $seen);
834
			};
835
836
			if (empty($dn)) {
837
				$dn = "";
838
			}
839
840
			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
841
		}
842
		$visibleGroups = $this->filterValidGroups($allGroups);
843
		return array_intersect_key($allGroups, $visibleGroups);
844
	}
845
846
	/**
847
	 * get a list of all users in a group
848
	 *
849
	 * @param string $gid
850
	 * @param string $search
851
	 * @param int $limit
852
	 * @param int $offset
853
	 * @return array with user ids
854
	 * @throws Exception
855
	 * @throws ServerNotAvailableException
856
	 */
857
	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
858
		if (!$this->enabled) {
859
			return [];
860
		}
861
		if (!$this->groupExists($gid)) {
862
			return [];
863
		}
864
		$search = $this->access->escapeFilterPart($search, true);
865
		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
866
		// check for cache of the exact query
867
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
868
		if (!is_null($groupUsers)) {
869
			return $groupUsers;
870
		}
871
872
		if ($limit === -1) {
873
			$limit = null;
874
		}
875
		// check for cache of the query without limit and offset
876
		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
877
		if (!is_null($groupUsers)) {
878
			$groupUsers = array_slice($groupUsers, $offset, $limit);
879
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
880
			return $groupUsers;
881
		}
882
883
		$groupDN = $this->access->groupname2dn($gid);
884
		if (!$groupDN) {
885
			// group couldn't be found, return empty resultset
886
			$this->access->connection->writeToCache($cacheKey, []);
887
			return [];
888
		}
889
890
		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
891
		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
892
		$members = $this->_groupMembers($groupDN);
893
		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $members of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$members is an empty array, thus ! $members is always true.
Loading history...
894
			//in case users could not be retrieved, return empty result set
895
			$this->access->connection->writeToCache($cacheKey, []);
896
			return [];
897
		}
898
899
		$groupUsers = [];
900
		$attrs = $this->access->userManager->getAttributes(true);
901
		foreach ($members as $member) {
902
			switch ($this->ldapGroupMemberAssocAttr) {
903
				case 'zimbramailforwardingaddress':
904
					//we get email addresses and need to convert them to uids
905
					$parts = explode('@', $member);
906
					$member = $parts[0];
907
					//no break needed because we just needed to remove the email part and now we have uids
908
				case 'memberuid':
909
					//we got uids, need to get their DNs to 'translate' them to user names
910
					$filter = $this->access->combineFilterWithAnd([
911
						str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
912
						$this->access->combineFilterWithAnd([
913
							$this->access->getFilterPartForUserSearch($search),
914
							$this->access->connection->ldapUserFilter
915
						])
916
					]);
917
					$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
918
					if (empty($ldap_users)) {
919
						break;
920
					}
921
					$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
922
					break;
923
				default:
924
					//we got DNs, check if we need to filter by search or we can give back all of them
925
					$uid = $this->access->dn2username($member);
926
					if (!$uid) {
927
						break;
928
					}
929
930
					$cacheKey = 'userExistsOnLDAP' . $uid;
931
					$userExists = $this->access->connection->getFromCache($cacheKey);
932
					if ($userExists === false) {
933
						break;
934
					}
935
					if ($userExists === null || $search !== '') {
936
						if (!$this->access->readAttribute($member,
937
							$this->access->connection->ldapUserDisplayName,
938
							$this->access->combineFilterWithAnd([
939
								$this->access->getFilterPartForUserSearch($search),
940
								$this->access->connection->ldapUserFilter
941
							]))) {
942
							if ($search === '') {
943
								$this->access->connection->writeToCache($cacheKey, false);
944
							}
945
							break;
946
						}
947
						$this->access->connection->writeToCache($cacheKey, true);
948
					}
949
					$groupUsers[] = $uid;
950
					break;
951
			}
952
		}
953
954
		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
955
		natsort($groupUsers);
956
		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
957
		$groupUsers = array_slice($groupUsers, $offset, $limit);
958
959
		$this->access->connection->writeToCache($cacheKey, $groupUsers);
960
961
		return $groupUsers;
962
	}
963
964
	/**
965
	 * returns the number of users in a group, who match the search term
966
	 *
967
	 * @param string $gid the internal group name
968
	 * @param string $search optional, a search string
969
	 * @return int|bool
970
	 * @throws Exception
971
	 * @throws ServerNotAvailableException
972
	 */
973
	public function countUsersInGroup($gid, $search = '') {
974
		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
975
			return $this->groupPluginManager->countUsersInGroup($gid, $search);
976
		}
977
978
		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
979
		if (!$this->enabled || !$this->groupExists($gid)) {
980
			return false;
981
		}
982
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
983
		if (!is_null($groupUsers)) {
984
			return $groupUsers;
985
		}
986
987
		$groupDN = $this->access->groupname2dn($gid);
988
		if (!$groupDN) {
989
			// group couldn't be found, return empty result set
990
			$this->access->connection->writeToCache($cacheKey, false);
991
			return false;
992
		}
993
994
		$members = $this->_groupMembers($groupDN);
995
		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
996
		if (!$members && $primaryUserCount === 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $members of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$members is an empty array, thus ! $members is always true.
Loading history...
997
			//in case users could not be retrieved, return empty result set
998
			$this->access->connection->writeToCache($cacheKey, false);
999
			return false;
1000
		}
1001
1002
		if ($search === '') {
1003
			$groupUsers = count($members) + $primaryUserCount;
1004
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
1005
			return $groupUsers;
1006
		}
1007
		$search = $this->access->escapeFilterPart($search, true);
1008
		$isMemberUid =
1009
			($this->ldapGroupMemberAssocAttr === 'memberuid' ||
1010
				$this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
1011
1012
		//we need to apply the search filter
1013
		//alternatives that need to be checked:
1014
		//a) get all users by search filter and array_intersect them
1015
		//b) a, but only when less than 1k 10k ?k users like it is
1016
		//c) put all DNs|uids in a LDAP filter, combine with the search string
1017
		//   and let it count.
1018
		//For now this is not important, because the only use of this method
1019
		//does not supply a search string
1020
		$groupUsers = [];
1021
		foreach ($members as $member) {
1022
			if ($isMemberUid) {
1023
				if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
1024
					//we get email addresses and need to convert them to uids
1025
					$parts = explode('@', $member);
1026
					$member = $parts[0];
1027
				}
1028
				//we got uids, need to get their DNs to 'translate' them to user names
1029
				$filter = $this->access->combineFilterWithAnd([
1030
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
1031
					$this->access->getFilterPartForUserSearch($search)
1032
				]);
1033
				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
1034
				if (count($ldap_users) < 1) {
1035
					continue;
1036
				}
1037
				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
1038
			} else {
1039
				//we need to apply the search filter now
1040
				if (!$this->access->readAttribute($member,
1041
					$this->access->connection->ldapUserDisplayName,
1042
					$this->access->getFilterPartForUserSearch($search))) {
1043
					continue;
1044
				}
1045
				// dn2username will also check if the users belong to the allowed base
1046
				if ($ncGroupId = $this->access->dn2username($member)) {
1047
					$groupUsers[] = $ncGroupId;
1048
				}
1049
			}
1050
		}
1051
1052
		//and get users that have the group as primary
1053
		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1054
1055
		return count($groupUsers) + $primaryUsers;
1056
	}
1057
1058
	/**
1059
	 * get a list of all groups using a paged search
1060
	 *
1061
	 * @param string $search
1062
	 * @param int $limit
1063
	 * @param int $offset
1064
	 * @return array with group names
1065
	 *
1066
	 * Returns a list with all groups
1067
	 * Uses a paged search if available to override a
1068
	 * server side search limit.
1069
	 * (active directory has a limit of 1000 by default)
1070
	 * @throws Exception
1071
	 */
1072
	public function getGroups($search = '', $limit = -1, $offset = 0) {
1073
		if (!$this->enabled) {
1074
			return [];
1075
		}
1076
		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1077
1078
		//Check cache before driving unnecessary searches
1079
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1080
		if (!is_null($ldap_groups)) {
1081
			return $ldap_groups;
1082
		}
1083
1084
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1085
		// error. With a limit of 0, we get 0 results. So we pass null.
1086
		if ($limit <= 0) {
1087
			$limit = null;
1088
		}
1089
		$filter = $this->access->combineFilterWithAnd([
1090
			$this->access->connection->ldapGroupFilter,
1091
			$this->access->getFilterPartForGroupSearch($search)
1092
		]);
1093
		$ldap_groups = $this->access->fetchListOfGroups($filter,
1094
			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1095
			$limit,
1096
			$offset);
1097
		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1098
1099
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1100
		return $ldap_groups;
1101
	}
1102
1103
	/**
1104
	 * check if a group exists
1105
	 *
1106
	 * @param string $gid
1107
	 * @return bool
1108
	 * @throws ServerNotAvailableException
1109
	 */
1110
	public function groupExists($gid) {
1111
		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1112
		if (!is_null($groupExists)) {
1113
			return (bool)$groupExists;
1114
		}
1115
1116
		//getting dn, if false the group does not exist. If dn, it may be mapped
1117
		//only, requires more checking.
1118
		$dn = $this->access->groupname2dn($gid);
1119
		if (!$dn) {
1120
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1121
			return false;
1122
		}
1123
1124
		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1125
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1126
			return false;
1127
		}
1128
1129
		//if group really still exists, we will be able to read its objectClass
1130
		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1131
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1132
			return false;
1133
		}
1134
1135
		$this->access->connection->writeToCache('groupExists' . $gid, true);
1136
		return true;
1137
	}
1138
1139
	/**
1140
	 * @throws ServerNotAvailableException
1141
	 * @throws Exception
1142
	 */
1143
	protected function filterValidGroups(array $listOfGroups): array {
1144
		$validGroupDNs = [];
1145
		foreach ($listOfGroups as $key => $item) {
1146
			$dn = is_string($item) ? $item : $item['dn'][0];
1147
			$gid = $this->access->dn2groupname($dn);
1148
			if (!$gid) {
1149
				continue;
1150
			}
1151
			if ($this->groupExists($gid)) {
1152
				$validGroupDNs[$key] = $item;
1153
			}
1154
		}
1155
		return $validGroupDNs;
1156
	}
1157
1158
	/**
1159
	 * Check if backend implements actions
1160
	 *
1161
	 * @param int $actions bitwise-or'ed actions
1162
	 * @return boolean
1163
	 *
1164
	 * Returns the supported actions as int to be
1165
	 * compared with GroupInterface::CREATE_GROUP etc.
1166
	 */
1167
	public function implementsActions($actions) {
1168
		return (bool)((GroupInterface::COUNT_USERS |
1169
				$this->groupPluginManager->getImplementedActions()) & $actions);
1170
	}
1171
1172
	/**
1173
	 * Return access for LDAP interaction.
1174
	 *
1175
	 * @return Access instance of Access for LDAP interaction
1176
	 */
1177
	public function getLDAPAccess($gid) {
1178
		return $this->access;
1179
	}
1180
1181
	/**
1182
	 * create a group
1183
	 *
1184
	 * @param string $gid
1185
	 * @return bool
1186
	 * @throws Exception
1187
	 * @throws ServerNotAvailableException
1188
	 */
1189
	public function createGroup($gid) {
1190
		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1191
			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1192
				//updates group mapping
1193
				$uuid = $this->access->getUUID($dn, false);
1194
				if (is_string($uuid)) {
1195
					$this->access->mapAndAnnounceIfApplicable(
1196
						$this->access->getGroupMapper(),
1197
						$dn,
1198
						$gid,
1199
						$uuid,
1200
						false
1201
					);
1202
					$this->access->cacheGroupExists($gid);
1203
				}
1204
			}
1205
			return $dn != null;
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $dn of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
1206
		}
1207
		throw new Exception('Could not create group in LDAP backend.');
1208
	}
1209
1210
	/**
1211
	 * delete a group
1212
	 *
1213
	 * @param string $gid gid of the group to delete
1214
	 * @return bool
1215
	 * @throws Exception
1216
	 */
1217
	public function deleteGroup($gid) {
1218
		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1219
			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1220
				#delete group in nextcloud internal db
1221
				$this->access->getGroupMapper()->unmap($gid);
1222
				$this->access->connection->writeToCache("groupExists" . $gid, false);
1223
			}
1224
			return $ret;
1225
		}
1226
		throw new Exception('Could not delete group in LDAP backend.');
1227
	}
1228
1229
	/**
1230
	 * Add a user to a group
1231
	 *
1232
	 * @param string $uid Name of the user to add to group
1233
	 * @param string $gid Name of the group in which add the user
1234
	 * @return bool
1235
	 * @throws Exception
1236
	 */
1237
	public function addToGroup($uid, $gid) {
1238
		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1239
			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1240
				$this->access->connection->clearCache();
1241
				unset($this->cachedGroupMembers[$gid]);
1242
			}
1243
			return $ret;
1244
		}
1245
		throw new Exception('Could not add user to group in LDAP backend.');
1246
	}
1247
1248
	/**
1249
	 * Removes a user from a group
1250
	 *
1251
	 * @param string $uid Name of the user to remove from group
1252
	 * @param string $gid Name of the group from which remove the user
1253
	 * @return bool
1254
	 * @throws Exception
1255
	 */
1256
	public function removeFromGroup($uid, $gid) {
1257
		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1258
			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1259
				$this->access->connection->clearCache();
1260
				unset($this->cachedGroupMembers[$gid]);
1261
			}
1262
			return $ret;
1263
		}
1264
		throw new Exception('Could not remove user from group in LDAP backend.');
1265
	}
1266
1267
	/**
1268
	 * Gets group details
1269
	 *
1270
	 * @param string $gid Name of the group
1271
	 * @return array|false
1272
	 * @throws Exception
1273
	 */
1274
	public function getGroupDetails($gid) {
1275
		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1276
			return $this->groupPluginManager->getGroupDetails($gid);
1277
		}
1278
		throw new Exception('Could not get group details in LDAP backend.');
1279
	}
1280
1281
	/**
1282
	 * Return LDAP connection resource from a cloned connection.
1283
	 * The cloned connection needs to be closed manually.
1284
	 * of the current access.
1285
	 *
1286
	 * @param string $gid
1287
	 * @return resource of the LDAP connection
1288
	 * @throws ServerNotAvailableException
1289
	 */
1290
	public function getNewLDAPConnection($gid) {
1291
		$connection = clone $this->access->getConnection();
1292
		return $connection->getConnectionResource();
1293
	}
1294
1295
	/**
1296
	 * @throws ServerNotAvailableException
1297
	 */
1298
	public function getDisplayName(string $gid): string {
1299
		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1300
			return $this->groupPluginManager->getDisplayName($gid);
1301
		}
1302
1303
		$cacheKey = 'group_getDisplayName' . $gid;
1304
		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1305
			return $displayName;
1306
		}
1307
1308
		$displayName = $this->access->readAttribute(
1309
			$this->access->groupname2dn($gid),
1310
			$this->access->connection->ldapGroupDisplayName);
1311
1312
		if ($displayName && (count($displayName) > 0)) {
1313
			$displayName = $displayName[0];
1314
			$this->access->connection->writeToCache($cacheKey, $displayName);
1315
			return $displayName;
1316
		}
1317
1318
		return '';
1319
	}
1320
}
1321