Passed
Push — master ( 326a04...579c70 )
by Blizzz
12:55 queued 11s
created

Group_LDAP   F

Complexity

Total Complexity 198

Size/Duplication

Total Lines 1225
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 577
dl 0
loc 1225
rs 2
c 0
b 0
f 0
wmc 198

38 Methods

Rating   Name   Duplication   Size   Complexity  
A getDisplayName() 0 21 5
A getNewLDAPConnection() 0 3 1
A createGroup() 0 19 4
A removeFromGroup() 0 9 3
A addToGroup() 0 9 3
A getGroupDetails() 0 5 2
A deleteGroup() 0 10 3
A prepareFilterForUsersHasGidNumber() 0 14 3
A getUserPrimaryGroup() 0 10 3
A groupExists() 0 27 5
A getGroups() 0 29 4
C walkNestedGroups() 0 32 12
A _groupMembers() 0 27 5
A getUsersInGidNumber() 0 19 3
A countUsersInPrimaryGroup() 0 14 3
A getUsersInPrimaryGroup() 0 19 3
F getUserGroups() 0 140 28
A getNameOfGroup() 0 15 2
A filterValidGroups() 0 13 5
B getGroupsByMember() 0 35 8
A _getGroupDNsFromMemberOf() 0 21 4
D usersInGroup() 0 105 20
A implementsActions() 0 3 1
A getEntryGroupID() 0 6 3
A primaryGroupID2Name() 0 18 4
A getUserGroupByGid() 0 10 3
A prepareFilterForUsersInPrimaryGroup() 0 14 3
A getUserGidNumber() 0 9 3
A __construct() 0 14 3
A getLDAPAccess() 0 2 1
A getUserPrimaryGroupIDs() 0 9 3
A getGroupPrimaryGroupID() 0 2 1
C inGroup() 0 86 16
A getDynamicGroupMembers() 0 35 5
C countUsersInGroup() 0 83 16
A gidNumber2Name() 0 14 3
A getEntryGidNumber() 0 6 3
A getGroupGidNumber() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Group_LDAP often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Group_LDAP, and based on these observations, apply Extract Interface, too.

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
		$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) {
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

331
	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...
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;
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

422
		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
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;
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...
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
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

492
			'objectsid=' . /** @scrutinizer ignore-type */ $domainObjectSid . '-' . $gid
Loading history...
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;
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

549
		$filterParts[] = 'primaryGroupID=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
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)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
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]);
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

746
				$groups = array_merge($groups, /** @scrutinizer ignore-type */ $this->cachedGroupsByMember[$uid]);
Loading history...
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)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
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)) {
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...
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) {
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...
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;
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...
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
}
1283