Passed
Push — master ( db782f...d72d9f )
by Morris
32:30 queued 18:44
created

Group_LDAP   F

Complexity

Total Complexity 189

Size/Duplication

Total Lines 1183
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 551
c 1
b 0
f 0
dl 0
loc 1183
rs 2
wmc 189

38 Methods

Rating   Name   Duplication   Size   Complexity  
A prepareFilterForUsersInPrimaryGroup() 0 14 3
A getGroupGidNumber() 0 2 1
A getUserGidNumber() 0 9 3
A getEntryGidNumber() 0 6 3
C inGroup() 0 79 14
A prepareFilterForUsersHasGidNumber() 0 14 3
A getUserPrimaryGroup() 0 10 3
A groupExists() 0 27 5
A getDisplayName() 0 21 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
D getUserGroups() 0 134 26
A createGroup() 0 19 4
A getNameOfGroup() 0 15 2
A filterValidGroups() 0 13 5
A getGroupsByMember() 0 24 6
A _getGroupDNsFromMemberOf() 0 21 4
D usersInGroup() 0 98 19
A removeFromGroup() 0 9 3
A implementsActions() 0 3 1
A getEntryGroupID() 0 6 3
A primaryGroupID2Name() 0 18 4
A getUserGroupByGid() 0 10 3
A addToGroup() 0 9 3
A getGroupDetails() 0 5 2
A getNewLDAPConnection() 0 3 1
A __construct() 0 13 3
A getLDAPAccess() 0 2 1
A getUserPrimaryGroupIDs() 0 9 3
A getGroupPrimaryGroupID() 0 2 1
A deleteGroup() 0 10 3
A getDynamicGroupMembers() 0 35 5
C countUsersInGroup() 0 78 14
A gidNumber2Name() 0 14 3

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 Victor Dubiniuk <[email protected]>
24
 * @author Vincent Petry <[email protected]>
25
 * @author Vinicius Cubas Brand <[email protected]>
26
 * @author Xuanwo <[email protected]>
27
 *
28
 * @license AGPL-3.0
29
 *
30
 * This code is free software: you can redistribute it and/or modify
31
 * it under the terms of the GNU Affero General Public License, version 3,
32
 * as published by the Free Software Foundation.
33
 *
34
 * This program is distributed in the hope that it will be useful,
35
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37
 * GNU Affero General Public License for more details.
38
 *
39
 * You should have received a copy of the GNU Affero General Public License, version 3,
40
 * along with this program. If not, see <http://www.gnu.org/licenses/>
41
 *
42
 */
43
44
namespace OCA\User_LDAP;
45
46
use Closure;
47
use Exception;
48
use OC;
49
use OC\Cache\CappedMemoryCache;
50
use OC\ServerNotAvailableException;
51
use OCP\Group\Backend\IGetDisplayNameBackend;
52
use OCP\GroupInterface;
53
use OCP\ILogger;
54
55
class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
56
	protected $enabled = false;
57
58
	/** @var string[] $cachedGroupMembers array of users with gid as key */
59
	protected $cachedGroupMembers;
60
	/** @var string[] $cachedGroupsByMember array of groups with uid as key */
61
	protected $cachedGroupsByMember;
62
	/** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
63
	protected $cachedNestedGroups;
64
	/** @var GroupPluginManager */
65
	protected $groupPluginManager;
66
	/** @var ILogger */
67
	protected $logger;
68
69
	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
70
		parent::__construct($access);
71
		$filter = $this->access->connection->ldapGroupFilter;
72
		$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
73
		if (!empty($filter) && !empty($gAssoc)) {
74
			$this->enabled = true;
75
		}
76
77
		$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...
78
		$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...
79
		$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...
80
		$this->groupPluginManager = $groupPluginManager;
81
		$this->logger = OC::$server->getLogger();
82
	}
83
84
	/**
85
	 * is user in group?
86
	 *
87
	 * @param string $uid uid of the user
88
	 * @param string $gid gid of the group
89
	 * @return bool
90
	 * @throws Exception
91
	 * @throws ServerNotAvailableException
92
	 */
93
	public function inGroup($uid, $gid) {
94
		if (!$this->enabled) {
95
			return false;
96
		}
97
		$cacheKey = 'inGroup' . $uid . ':' . $gid;
98
		$inGroup = $this->access->connection->getFromCache($cacheKey);
99
		if (!is_null($inGroup)) {
100
			return (bool)$inGroup;
101
		}
102
103
		$userDN = $this->access->username2dn($uid);
104
105
		if (isset($this->cachedGroupMembers[$gid])) {
106
			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

106
			return in_array($userDN, /** @scrutinizer ignore-type */ $this->cachedGroupMembers[$gid]);
Loading history...
107
		}
108
109
		$cacheKeyMembers = 'inGroup-members:' . $gid;
110
		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111
		if (!is_null($members)) {
112
			$this->cachedGroupMembers[$gid] = $members;
113
			$isInGroup = in_array($userDN, $members, true);
114
			$this->access->connection->writeToCache($cacheKey, $isInGroup);
115
			return $isInGroup;
116
		}
117
118
		$groupDN = $this->access->groupname2dn($gid);
119
		// just in case
120
		if (!$groupDN || !$userDN) {
121
			$this->access->connection->writeToCache($cacheKey, false);
122
			return false;
123
		}
124
125
		//check primary group first
126
		if ($gid === $this->getUserPrimaryGroup($userDN)) {
127
			$this->access->connection->writeToCache($cacheKey, true);
128
			return true;
129
		}
130
131
		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
		$members = $this->_groupMembers($groupDN);
133
		if (!is_array($members) || count($members) === 0) {
0 ignored issues
show
introduced by
The condition is_array($members) is always true.
Loading history...
134
			$this->access->connection->writeToCache($cacheKey, false);
135
			return false;
136
		}
137
138
		//extra work if we don't get back user DNs
139
		if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140
			$requestAttributes = $this->access->userManager->getAttributes(true);
141
			$dns = [];
142
			$filterParts = [];
143
			$bytes = 0;
144
			foreach ($members as $mid) {
145
				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
146
				$filterParts[] = $filter;
147
				$bytes += strlen($filter);
148
				if ($bytes >= 9000000) {
149
					// AD has a default input buffer of 10 MB, we do not want
150
					// to take even the chance to exceed it
151
					$filter = $this->access->combineFilterWithOr($filterParts);
152
					$bytes = 0;
153
					$filterParts = [];
154
					$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
155
					$dns = array_merge($dns, $users);
156
				}
157
			}
158
			if (count($filterParts) > 0) {
159
				$filter = $this->access->combineFilterWithOr($filterParts);
160
				$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
161
				$dns = array_merge($dns, $users);
162
			}
163
			$members = $dns;
164
		}
165
166
		$isInGroup = in_array($userDN, $members);
167
		$this->access->connection->writeToCache($cacheKey, $isInGroup);
168
		$this->access->connection->writeToCache($cacheKeyMembers, $members);
169
		$this->cachedGroupMembers[$gid] = $members;
170
171
		return $isInGroup;
172
	}
173
174
	/**
175
	 * For a group that has user membership defined by an LDAP search url
176
	 * attribute returns the users that match the search url otherwise returns
177
	 * an empty array.
178
	 *
179
	 * @throws ServerNotAvailableException
180
	 */
181
	public function getDynamicGroupMembers(string $dnGroup): array {
182
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
183
184
		if (empty($dynamicGroupMemberURL)) {
185
			return [];
186
		}
187
188
		$dynamicMembers = [];
189
		$memberURLs = $this->access->readAttribute(
190
			$dnGroup,
191
			$dynamicGroupMemberURL,
192
			$this->access->connection->ldapGroupFilter
193
		);
194
		if ($memberURLs !== false) {
195
			// this group has the 'memberURL' attribute so this is a dynamic group
196
			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
197
			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
198
			$pos = strpos($memberURLs[0], '(');
199
			if ($pos !== false) {
200
				$memberUrlFilter = substr($memberURLs[0], $pos);
201
				$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

201
				$foundMembers = $this->access->searchUsers($memberUrlFilter, /** @scrutinizer ignore-type */ 'dn');
Loading history...
202
				$dynamicMembers = [];
203
				foreach ($foundMembers as $value) {
204
					$dynamicMembers[$value['dn'][0]] = 1;
205
				}
206
			} else {
207
				$this->logger->debug('No search filter found on member url of group {dn}',
208
					[
209
						'app' => 'user_ldap',
210
						'dn' => $dnGroup,
211
					]
212
				);
213
			}
214
		}
215
		return $dynamicMembers;
216
	}
217
218
	/**
219
	 * @throws ServerNotAvailableException
220
	 */
221
	private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
222
		if ($seen === null) {
223
			$seen = [];
224
		}
225
		$allMembers = [];
226
		if (array_key_exists($dnGroup, $seen)) {
227
			return [];
228
		}
229
		// used extensively in cron job, caching makes sense for nested groups
230
		$cacheKey = '_groupMembers' . $dnGroup;
231
		$groupMembers = $this->access->connection->getFromCache($cacheKey);
232
		if ($groupMembers !== null) {
233
			return $groupMembers;
234
		}
235
		$seen[$dnGroup] = 1;
236
		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
237
		if (is_array($members)) {
238
			$fetcher = function ($memberDN, &$seen) {
239
				return $this->_groupMembers($memberDN, $seen);
240
			};
241
			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
242
		}
243
244
		$allMembers += $this->getDynamicGroupMembers($dnGroup);
245
246
		$this->access->connection->writeToCache($cacheKey, $allMembers);
247
		return $allMembers;
248
	}
249
250
	/**
251
	 * @throws ServerNotAvailableException
252
	 */
253
	private function _getGroupDNsFromMemberOf(string $dn): array {
254
		$groups = $this->access->readAttribute($dn, 'memberOf');
255
		if (!is_array($groups)) {
256
			return [];
257
		}
258
259
		$fetcher = function ($groupDN) {
260
			if (isset($this->cachedNestedGroups[$groupDN])) {
261
				$nestedGroups = $this->cachedNestedGroups[$groupDN];
262
			} else {
263
				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
264
				if (!is_array($nestedGroups)) {
265
					$nestedGroups = [];
266
				}
267
				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
268
			}
269
			return $nestedGroups;
270
		};
271
272
		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
273
		return $this->filterValidGroups($groups);
274
	}
275
276
	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
277
		$nesting = (int)$this->access->connection->ldapNestedGroups;
278
		// depending on the input, we either have a list of DNs or a list of LDAP records
279
		// also, the output expects either DNs or records. Testing the first element should suffice.
280
		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
281
282
		if ($nesting !== 1) {
283
			if ($recordMode) {
284
				// the keys are numeric, but should hold the DN
285
				return array_reduce($list, function ($transformed, $record) use ($dn) {
286
					if ($record['dn'][0] != $dn) {
287
						$transformed[$record['dn'][0]] = $record;
288
					}
289
					return $transformed;
290
				}, []);
291
			}
292
			return $list;
293
		}
294
295
		$seen = [];
296
		while ($record = array_pop($list)) {
297
			$recordDN = $recordMode ? $record['dn'][0] : $record;
298
			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
299
				// Prevent loops
300
				continue;
301
			}
302
			$fetched = $fetcher($record, $seen);
303
			$list = array_merge($list, $fetched);
304
			$seen[$recordDN] = $record;
305
		}
306
307
		return $recordMode ? $seen : array_keys($seen);
308
	}
309
310
	/**
311
	 * translates a gidNumber into an ownCloud internal name
312
	 *
313
	 * @return string|bool
314
	 * @throws Exception
315
	 * @throws ServerNotAvailableException
316
	 */
317
	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

317
	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...
318
		$cacheKey = 'gidNumberToName' . $gid;
319
		$groupName = $this->access->connection->getFromCache($cacheKey);
320
		if (!is_null($groupName) && isset($groupName)) {
321
			return $groupName;
322
		}
323
324
		//we need to get the DN from LDAP
325
		$filter = $this->access->combineFilterWithAnd([
326
			$this->access->connection->ldapGroupFilter,
327
			'objectClass=posixGroup',
328
			$this->access->connection->ldapGidNumber . '=' . $gid
329
		]);
330
		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
331
	}
332
333
	/**
334
	 * @throws ServerNotAvailableException
335
	 * @throws Exception
336
	 */
337
	private function getNameOfGroup(string $filter, string $cacheKey) {
338
		$result = $this->access->searchGroups($filter, ['dn'], 1);
339
		if (empty($result)) {
340
			return null;
341
		}
342
		$dn = $result[0]['dn'][0];
343
344
		//and now the group name
345
		//NOTE once we have separate Nextcloud group IDs and group names we can
346
		//directly read the display name attribute instead of the DN
347
		$name = $this->access->dn2groupname($dn);
348
349
		$this->access->connection->writeToCache($cacheKey, $name);
350
351
		return $name;
352
	}
353
354
	/**
355
	 * returns the entry's gidNumber
356
	 *
357
	 * @return string|bool
358
	 * @throws ServerNotAvailableException
359
	 */
360
	private function getEntryGidNumber(string $dn, string $attribute) {
361
		$value = $this->access->readAttribute($dn, $attribute);
362
		if (is_array($value) && !empty($value)) {
363
			return $value[0];
364
		}
365
		return false;
366
	}
367
368
	/**
369
	 * @return string|bool
370
	 * @throws ServerNotAvailableException
371
	 */
372
	public function getGroupGidNumber(string $dn) {
373
		return $this->getEntryGidNumber($dn, 'gidNumber');
374
	}
375
376
	/**
377
	 * returns the user's gidNumber
378
	 *
379
	 * @return string|bool
380
	 * @throws ServerNotAvailableException
381
	 */
382
	public function getUserGidNumber(string $dn) {
383
		$gidNumber = false;
384
		if ($this->access->connection->hasGidNumber) {
385
			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
386
			if ($gidNumber === false) {
387
				$this->access->connection->hasGidNumber = false;
388
			}
389
		}
390
		return $gidNumber;
391
	}
392
393
	/**
394
	 * @throws ServerNotAvailableException
395
	 * @throws Exception
396
	 */
397
	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
398
		$groupID = $this->getGroupGidNumber($groupDN);
399
		if ($groupID === false) {
400
			throw new Exception('Not a valid group');
401
		}
402
403
		$filterParts = [];
404
		$filterParts[] = $this->access->getFilterForUserCount();
405
		if ($search !== '') {
406
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
407
		}
408
		$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

408
		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
409
410
		return $this->access->combineFilterWithAnd($filterParts);
411
	}
412
413
	/**
414
	 * returns a list of users that have the given group as gid number
415
	 *
416
	 * @throws ServerNotAvailableException
417
	 */
418
	public function getUsersInGidNumber(
419
		string $groupDN,
420
		string $search = '',
421
		?int $limit = -1,
422
		?int $offset = 0
423
	): array {
424
		try {
425
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
426
			$users = $this->access->fetchListOfUsers(
427
				$filter,
428
				[$this->access->connection->ldapUserDisplayName, 'dn'],
429
				$limit,
430
				$offset
431
			);
432
			return $this->access->nextcloudUserNames($users);
433
		} catch (ServerNotAvailableException $e) {
434
			throw $e;
435
		} catch (Exception $e) {
436
			return [];
437
		}
438
	}
439
440
	/**
441
	 * @throws ServerNotAvailableException
442
	 * @return bool
443
	 */
444
	public function getUserGroupByGid(string $dn) {
445
		$groupID = $this->getUserGidNumber($dn);
446
		if ($groupID !== false) {
447
			$groupName = $this->gidNumber2Name($groupID, $dn);
448
			if ($groupName !== false) {
449
				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...
450
			}
451
		}
452
453
		return false;
454
	}
455
456
	/**
457
	 * translates a primary group ID into an Nextcloud internal name
458
	 *
459
	 * @return string|bool
460
	 * @throws Exception
461
	 * @throws ServerNotAvailableException
462
	 */
463
	public function primaryGroupID2Name(string $gid, string $dn) {
464
		$cacheKey = 'primaryGroupIDtoName';
465
		$groupNames = $this->access->connection->getFromCache($cacheKey);
466
		if (!is_null($groupNames) && isset($groupNames[$gid])) {
467
			return $groupNames[$gid];
468
		}
469
470
		$domainObjectSid = $this->access->getSID($dn);
471
		if ($domainObjectSid === false) {
472
			return false;
473
		}
474
475
		//we need to get the DN from LDAP
476
		$filter = $this->access->combineFilterWithAnd([
477
			$this->access->connection->ldapGroupFilter,
478
			'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

478
			'objectsid=' . /** @scrutinizer ignore-type */ $domainObjectSid . '-' . $gid
Loading history...
479
		]);
480
		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
481
	}
482
483
	/**
484
	 * returns the entry's primary group ID
485
	 *
486
	 * @return string|bool
487
	 * @throws ServerNotAvailableException
488
	 */
489
	private function getEntryGroupID(string $dn, string $attribute) {
490
		$value = $this->access->readAttribute($dn, $attribute);
491
		if (is_array($value) && !empty($value)) {
492
			return $value[0];
493
		}
494
		return false;
495
	}
496
497
	/**
498
	 * @return string|bool
499
	 * @throws ServerNotAvailableException
500
	 */
501
	public function getGroupPrimaryGroupID(string $dn) {
502
		return $this->getEntryGroupID($dn, 'primaryGroupToken');
503
	}
504
505
	/**
506
	 * @return string|bool
507
	 * @throws ServerNotAvailableException
508
	 */
509
	public function getUserPrimaryGroupIDs(string $dn) {
510
		$primaryGroupID = false;
511
		if ($this->access->connection->hasPrimaryGroups) {
512
			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
513
			if ($primaryGroupID === false) {
514
				$this->access->connection->hasPrimaryGroups = false;
515
			}
516
		}
517
		return $primaryGroupID;
518
	}
519
520
	/**
521
	 * @throws Exception
522
	 * @throws ServerNotAvailableException
523
	 */
524
	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
525
		$groupID = $this->getGroupPrimaryGroupID($groupDN);
526
		if ($groupID === false) {
527
			throw new Exception('Not a valid group');
528
		}
529
530
		$filterParts = [];
531
		$filterParts[] = $this->access->getFilterForUserCount();
532
		if ($search !== '') {
533
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
		}
535
		$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

535
		$filterParts[] = 'primaryGroupID=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
536
537
		return $this->access->combineFilterWithAnd($filterParts);
538
	}
539
540
	/**
541
	 * @throws ServerNotAvailableException
542
	 */
543
	public function getUsersInPrimaryGroup(
544
		string $groupDN,
545
		string $search = '',
546
		?int $limit = -1,
547
		?int $offset = 0
548
	): array {
549
		try {
550
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
551
			$users = $this->access->fetchListOfUsers(
552
				$filter,
553
				[$this->access->connection->ldapUserDisplayName, 'dn'],
554
				$limit,
555
				$offset
556
			);
557
			return $this->access->nextcloudUserNames($users);
558
		} catch (ServerNotAvailableException $e) {
559
			throw $e;
560
		} catch (Exception $e) {
561
			return [];
562
		}
563
	}
564
565
	/**
566
	 * @throws ServerNotAvailableException
567
	 */
568
	public function countUsersInPrimaryGroup(
569
		string $groupDN,
570
		string $search = '',
571
		int $limit = -1,
572
		int $offset = 0
573
	): int {
574
		try {
575
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
577
			return (int)$users;
578
		} catch (ServerNotAvailableException $e) {
579
			throw $e;
580
		} catch (Exception $e) {
581
			return 0;
582
		}
583
	}
584
585
	/**
586
	 * @return string|bool
587
	 * @throws ServerNotAvailableException
588
	 */
589
	public function getUserPrimaryGroup(string $dn) {
590
		$groupID = $this->getUserPrimaryGroupIDs($dn);
591
		if ($groupID !== false) {
592
			$groupName = $this->primaryGroupID2Name($groupID, $dn);
593
			if ($groupName !== false) {
594
				return $groupName;
595
			}
596
		}
597
598
		return false;
599
	}
600
601
	/**
602
	 * This function fetches all groups a user belongs to. It does not check
603
	 * if the user exists at all.
604
	 *
605
	 * This function includes groups based on dynamic group membership.
606
	 *
607
	 * @param string $uid Name of the user
608
	 * @return array with group names
609
	 * @throws Exception
610
	 * @throws ServerNotAvailableException
611
	 */
612
	public function getUserGroups($uid) {
613
		if (!$this->enabled) {
614
			return [];
615
		}
616
		$cacheKey = 'getUserGroups' . $uid;
617
		$userGroups = $this->access->connection->getFromCache($cacheKey);
618
		if (!is_null($userGroups)) {
619
			return $userGroups;
620
		}
621
		$userDN = $this->access->username2dn($uid);
622
		if (!$userDN) {
623
			$this->access->connection->writeToCache($cacheKey, []);
624
			return [];
625
		}
626
627
		$groups = [];
628
		$primaryGroup = $this->getUserPrimaryGroup($userDN);
629
		$gidGroupName = $this->getUserGroupByGid($userDN);
630
631
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
632
633
		if (!empty($dynamicGroupMemberURL)) {
634
			// look through dynamic groups to add them to the result array if needed
635
			$groupsToMatch = $this->access->fetchListOfGroups(
636
				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
637
			foreach ($groupsToMatch as $dynamicGroup) {
638
				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
639
					continue;
640
				}
641
				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
642
				if ($pos !== false) {
643
					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
644
					// apply filter via ldap search to see if this user is in this
645
					// dynamic group
646
					$userMatch = $this->access->readAttribute(
647
						$userDN,
648
						$this->access->connection->ldapUserDisplayName,
649
						$memberUrlFilter
650
					);
651
					if ($userMatch !== false) {
652
						// match found so this user is in this group
653
						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
654
						if (is_string($groupName)) {
655
							// be sure to never return false if the dn could not be
656
							// resolved to a name, for whatever reason.
657
							$groups[] = $groupName;
658
						}
659
					}
660
				} else {
661
					$this->logger->debug('No search filter found on member url of group {dn}',
662
						[
663
							'app' => 'user_ldap',
664
							'dn' => $dynamicGroup,
665
						]
666
					);
667
				}
668
			}
669
		}
670
671
		// if possible, read out membership via memberOf. It's far faster than
672
		// performing a search, which still is a fallback later.
673
		// memberof doesn't support memberuid, so skip it here.
674
		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
675
			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
676
			&& strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
677
		) {
678
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
679
			if (is_array($groupDNs)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
680
				foreach ($groupDNs as $dn) {
681
					$groupName = $this->access->dn2groupname($dn);
682
					if (is_string($groupName)) {
683
						// be sure to never return false if the dn could not be
684
						// resolved to a name, for whatever reason.
685
						$groups[] = $groupName;
686
					}
687
				}
688
			}
689
690
			if ($primaryGroup !== false) {
691
				$groups[] = $primaryGroup;
692
			}
693
			if ($gidGroupName !== false) {
694
				$groups[] = $gidGroupName;
695
			}
696
			$this->access->connection->writeToCache($cacheKey, $groups);
697
			return $groups;
698
		}
699
700
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
701
		if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
702
			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
703
		) {
704
			$uid = $userDN;
705
		} elseif (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
706
			$result = $this->access->readAttribute($userDN, 'uid');
707
			if ($result === false) {
708
				$this->logger->debug('No uid attribute found for DN {dn} on {host}',
709
					[
710
						'app' => 'user_ldap',
711
						'dn' => $userDN,
712
						'host' => $this->access->connection->ldapHost,
713
					]
714
				);
715
				$uid = false;
716
			} else {
717
				$uid = $result[0];
718
			}
719
		} else {
720
			// just in case
721
			$uid = $userDN;
722
		}
723
724
		if ($uid !== false) {
725
			if (isset($this->cachedGroupsByMember[$uid])) {
726
				$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

726
				$groups = array_merge($groups, /** @scrutinizer ignore-type */ $this->cachedGroupsByMember[$uid]);
Loading history...
727
			} else {
728
				$groupsByMember = array_values($this->getGroupsByMember($uid));
729
				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
730
				$this->cachedGroupsByMember[$uid] = $groupsByMember;
731
				$groups = array_merge($groups, $groupsByMember);
732
			}
733
		}
734
735
		if ($primaryGroup !== false) {
736
			$groups[] = $primaryGroup;
737
		}
738
		if ($gidGroupName !== false) {
739
			$groups[] = $gidGroupName;
740
		}
741
742
		$groups = array_unique($groups, SORT_LOCALE_STRING);
743
		$this->access->connection->writeToCache($cacheKey, $groups);
744
745
		return $groups;
746
	}
747
748
	/**
749
	 * @throws ServerNotAvailableException
750
	 */
751
	private function getGroupsByMember(string $dn, array &$seen = null): array {
752
		if ($seen === null) {
753
			$seen = [];
754
		}
755
		if (array_key_exists($dn, $seen)) {
756
			// avoid loops
757
			return [];
758
		}
759
		$allGroups = [];
760
		$seen[$dn] = true;
761
		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
762
		$groups = $this->access->fetchListOfGroups($filter,
763
			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
764
		if (is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
765
			$fetcher = function ($dn, &$seen) {
766
				if (is_array($dn) && isset($dn['dn'][0])) {
767
					$dn = $dn['dn'][0];
768
				}
769
				return $this->getGroupsByMember($dn, $seen);
770
			};
771
			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
772
		}
773
		$visibleGroups = $this->filterValidGroups($allGroups);
774
		return array_intersect_key($allGroups, $visibleGroups);
775
	}
776
777
	/**
778
	 * get a list of all users in a group
779
	 *
780
	 * @param string $gid
781
	 * @param string $search
782
	 * @param int $limit
783
	 * @param int $offset
784
	 * @return array with user ids
785
	 * @throws Exception
786
	 * @throws ServerNotAvailableException
787
	 */
788
	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
789
		if (!$this->enabled) {
790
			return [];
791
		}
792
		if (!$this->groupExists($gid)) {
793
			return [];
794
		}
795
		$search = $this->access->escapeFilterPart($search, true);
796
		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
797
		// check for cache of the exact query
798
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
799
		if (!is_null($groupUsers)) {
800
			return $groupUsers;
801
		}
802
803
		if ($limit === -1) {
804
			$limit = null;
805
		}
806
		// check for cache of the query without limit and offset
807
		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
808
		if (!is_null($groupUsers)) {
809
			$groupUsers = array_slice($groupUsers, $offset, $limit);
810
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
811
			return $groupUsers;
812
		}
813
814
		$groupDN = $this->access->groupname2dn($gid);
815
		if (!$groupDN) {
816
			// group couldn't be found, return empty resultset
817
			$this->access->connection->writeToCache($cacheKey, []);
818
			return [];
819
		}
820
821
		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
822
		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
823
		$members = $this->_groupMembers($groupDN);
824
		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...
825
			//in case users could not be retrieved, return empty result set
826
			$this->access->connection->writeToCache($cacheKey, []);
827
			return [];
828
		}
829
830
		$groupUsers = [];
831
		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
832
		$attrs = $this->access->userManager->getAttributes(true);
833
		foreach ($members as $member) {
834
			if ($isMemberUid) {
835
				//we got uids, need to get their DNs to 'translate' them to user names
836
				$filter = $this->access->combineFilterWithAnd([
837
					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
838
					$this->access->combineFilterWithAnd([
839
						$this->access->getFilterPartForUserSearch($search),
840
						$this->access->connection->ldapUserFilter
841
					])
842
				]);
843
				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
844
				if (count($ldap_users) < 1) {
845
					continue;
846
				}
847
				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
848
			} else {
849
				//we got DNs, check if we need to filter by search or we can give back all of them
850
				$uid = $this->access->dn2username($member);
851
				if (!$uid) {
852
					continue;
853
				}
854
855
				$cacheKey = 'userExistsOnLDAP' . $uid;
856
				$userExists = $this->access->connection->getFromCache($cacheKey);
857
				if ($userExists === false) {
858
					continue;
859
				}
860
				if ($userExists === null || $search !== '') {
861
					if (!$this->access->readAttribute($member,
862
						$this->access->connection->ldapUserDisplayName,
863
						$this->access->combineFilterWithAnd([
864
							$this->access->getFilterPartForUserSearch($search),
865
							$this->access->connection->ldapUserFilter
866
						]))) {
867
						if ($search === '') {
868
							$this->access->connection->writeToCache($cacheKey, false);
869
						}
870
						continue;
871
					}
872
					$this->access->connection->writeToCache($cacheKey, true);
873
				}
874
				$groupUsers[] = $uid;
875
			}
876
		}
877
878
		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
879
		natsort($groupUsers);
880
		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
881
		$groupUsers = array_slice($groupUsers, $offset, $limit);
882
883
		$this->access->connection->writeToCache($cacheKey, $groupUsers);
884
885
		return $groupUsers;
886
	}
887
888
	/**
889
	 * returns the number of users in a group, who match the search term
890
	 *
891
	 * @param string $gid the internal group name
892
	 * @param string $search optional, a search string
893
	 * @return int|bool
894
	 * @throws Exception
895
	 * @throws ServerNotAvailableException
896
	 */
897
	public function countUsersInGroup($gid, $search = '') {
898
		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
899
			return $this->groupPluginManager->countUsersInGroup($gid, $search);
900
		}
901
902
		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
903
		if (!$this->enabled || !$this->groupExists($gid)) {
904
			return false;
905
		}
906
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
907
		if (!is_null($groupUsers)) {
908
			return $groupUsers;
909
		}
910
911
		$groupDN = $this->access->groupname2dn($gid);
912
		if (!$groupDN) {
913
			// group couldn't be found, return empty result set
914
			$this->access->connection->writeToCache($cacheKey, false);
915
			return false;
916
		}
917
918
		$members = $this->_groupMembers($groupDN);
919
		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
920
		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...
921
			//in case users could not be retrieved, return empty result set
922
			$this->access->connection->writeToCache($cacheKey, false);
923
			return false;
924
		}
925
926
		if ($search === '') {
927
			$groupUsers = count($members) + $primaryUserCount;
928
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
929
			return $groupUsers;
930
		}
931
		$search = $this->access->escapeFilterPart($search, true);
932
		$isMemberUid =
933
			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
934
				=== 'memberuid');
935
936
		//we need to apply the search filter
937
		//alternatives that need to be checked:
938
		//a) get all users by search filter and array_intersect them
939
		//b) a, but only when less than 1k 10k ?k users like it is
940
		//c) put all DNs|uids in a LDAP filter, combine with the search string
941
		//   and let it count.
942
		//For now this is not important, because the only use of this method
943
		//does not supply a search string
944
		$groupUsers = [];
945
		foreach ($members as $member) {
946
			if ($isMemberUid) {
947
				//we got uids, need to get their DNs to 'translate' them to user names
948
				$filter = $this->access->combineFilterWithAnd([
949
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
950
					$this->access->getFilterPartForUserSearch($search)
951
				]);
952
				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
953
				if (count($ldap_users) < 1) {
954
					continue;
955
				}
956
				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
957
			} else {
958
				//we need to apply the search filter now
959
				if (!$this->access->readAttribute($member,
960
					$this->access->connection->ldapUserDisplayName,
961
					$this->access->getFilterPartForUserSearch($search))) {
962
					continue;
963
				}
964
				// dn2username will also check if the users belong to the allowed base
965
				if ($ncGroupId = $this->access->dn2username($member)) {
966
					$groupUsers[] = $ncGroupId;
967
				}
968
			}
969
		}
970
971
		//and get users that have the group as primary
972
		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
973
974
		return count($groupUsers) + $primaryUsers;
975
	}
976
977
	/**
978
	 * get a list of all groups using a paged search
979
	 *
980
	 * @param string $search
981
	 * @param int $limit
982
	 * @param int $offset
983
	 * @return array with group names
984
	 *
985
	 * Returns a list with all groups
986
	 * Uses a paged search if available to override a
987
	 * server side search limit.
988
	 * (active directory has a limit of 1000 by default)
989
	 * @throws Exception
990
	 */
991
	public function getGroups($search = '', $limit = -1, $offset = 0) {
992
		if (!$this->enabled) {
993
			return [];
994
		}
995
		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
996
997
		//Check cache before driving unnecessary searches
998
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
		if (!is_null($ldap_groups)) {
1000
			return $ldap_groups;
1001
		}
1002
1003
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
		// error. With a limit of 0, we get 0 results. So we pass null.
1005
		if ($limit <= 0) {
1006
			$limit = null;
1007
		}
1008
		$filter = $this->access->combineFilterWithAnd([
1009
			$this->access->connection->ldapGroupFilter,
1010
			$this->access->getFilterPartForGroupSearch($search)
1011
		]);
1012
		$ldap_groups = $this->access->fetchListOfGroups($filter,
1013
			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1014
			$limit,
1015
			$offset);
1016
		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1017
1018
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1019
		return $ldap_groups;
1020
	}
1021
1022
	/**
1023
	 * check if a group exists
1024
	 *
1025
	 * @param string $gid
1026
	 * @return bool
1027
	 * @throws ServerNotAvailableException
1028
	 */
1029
	public function groupExists($gid) {
1030
		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1031
		if (!is_null($groupExists)) {
1032
			return (bool)$groupExists;
1033
		}
1034
1035
		//getting dn, if false the group does not exist. If dn, it may be mapped
1036
		//only, requires more checking.
1037
		$dn = $this->access->groupname2dn($gid);
1038
		if (!$dn) {
1039
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1040
			return false;
1041
		}
1042
1043
		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1044
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1045
			return false;
1046
		}
1047
1048
		//if group really still exists, we will be able to read its objectClass
1049
		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1050
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1051
			return false;
1052
		}
1053
1054
		$this->access->connection->writeToCache('groupExists' . $gid, true);
1055
		return true;
1056
	}
1057
1058
	/**
1059
	 * @throws ServerNotAvailableException
1060
	 * @throws Exception
1061
	 */
1062
	protected function filterValidGroups(array $listOfGroups): array {
1063
		$validGroupDNs = [];
1064
		foreach ($listOfGroups as $key => $item) {
1065
			$dn = is_string($item) ? $item : $item['dn'][0];
1066
			$gid = $this->access->dn2groupname($dn);
1067
			if (!$gid) {
1068
				continue;
1069
			}
1070
			if ($this->groupExists($gid)) {
1071
				$validGroupDNs[$key] = $item;
1072
			}
1073
		}
1074
		return $validGroupDNs;
1075
	}
1076
1077
	/**
1078
	 * Check if backend implements actions
1079
	 *
1080
	 * @param int $actions bitwise-or'ed actions
1081
	 * @return boolean
1082
	 *
1083
	 * Returns the supported actions as int to be
1084
	 * compared with GroupInterface::CREATE_GROUP etc.
1085
	 */
1086
	public function implementsActions($actions) {
1087
		return (bool)((GroupInterface::COUNT_USERS |
1088
				$this->groupPluginManager->getImplementedActions()) & $actions);
1089
	}
1090
1091
	/**
1092
	 * Return access for LDAP interaction.
1093
	 *
1094
	 * @return Access instance of Access for LDAP interaction
1095
	 */
1096
	public function getLDAPAccess($gid) {
1097
		return $this->access;
1098
	}
1099
1100
	/**
1101
	 * create a group
1102
	 *
1103
	 * @param string $gid
1104
	 * @return bool
1105
	 * @throws Exception
1106
	 * @throws ServerNotAvailableException
1107
	 */
1108
	public function createGroup($gid) {
1109
		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1110
			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1111
				//updates group mapping
1112
				$uuid = $this->access->getUUID($dn, false);
1113
				if (is_string($uuid)) {
1114
					$this->access->mapAndAnnounceIfApplicable(
1115
						$this->access->getGroupMapper(),
1116
						$dn,
1117
						$gid,
1118
						$uuid,
1119
						false
1120
					);
1121
					$this->access->cacheGroupExists($gid);
1122
				}
1123
			}
1124
			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...
1125
		}
1126
		throw new Exception('Could not create group in LDAP backend.');
1127
	}
1128
1129
	/**
1130
	 * delete a group
1131
	 *
1132
	 * @param string $gid gid of the group to delete
1133
	 * @return bool
1134
	 * @throws Exception
1135
	 */
1136
	public function deleteGroup($gid) {
1137
		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1138
			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1139
				#delete group in nextcloud internal db
1140
				$this->access->getGroupMapper()->unmap($gid);
1141
				$this->access->connection->writeToCache("groupExists" . $gid, false);
1142
			}
1143
			return $ret;
1144
		}
1145
		throw new Exception('Could not delete group in LDAP backend.');
1146
	}
1147
1148
	/**
1149
	 * Add a user to a group
1150
	 *
1151
	 * @param string $uid Name of the user to add to group
1152
	 * @param string $gid Name of the group in which add the user
1153
	 * @return bool
1154
	 * @throws Exception
1155
	 */
1156
	public function addToGroup($uid, $gid) {
1157
		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1158
			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1159
				$this->access->connection->clearCache();
1160
				unset($this->cachedGroupMembers[$gid]);
1161
			}
1162
			return $ret;
1163
		}
1164
		throw new Exception('Could not add user to group in LDAP backend.');
1165
	}
1166
1167
	/**
1168
	 * Removes a user from a group
1169
	 *
1170
	 * @param string $uid Name of the user to remove from group
1171
	 * @param string $gid Name of the group from which remove the user
1172
	 * @return bool
1173
	 * @throws Exception
1174
	 */
1175
	public function removeFromGroup($uid, $gid) {
1176
		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1177
			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1178
				$this->access->connection->clearCache();
1179
				unset($this->cachedGroupMembers[$gid]);
1180
			}
1181
			return $ret;
1182
		}
1183
		throw new Exception('Could not remove user from group in LDAP backend.');
1184
	}
1185
1186
	/**
1187
	 * Gets group details
1188
	 *
1189
	 * @param string $gid Name of the group
1190
	 * @return array|false
1191
	 * @throws Exception
1192
	 */
1193
	public function getGroupDetails($gid) {
1194
		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1195
			return $this->groupPluginManager->getGroupDetails($gid);
1196
		}
1197
		throw new Exception('Could not get group details in LDAP backend.');
1198
	}
1199
1200
	/**
1201
	 * Return LDAP connection resource from a cloned connection.
1202
	 * The cloned connection needs to be closed manually.
1203
	 * of the current access.
1204
	 *
1205
	 * @param string $gid
1206
	 * @return resource of the LDAP connection
1207
	 * @throws ServerNotAvailableException
1208
	 */
1209
	public function getNewLDAPConnection($gid) {
1210
		$connection = clone $this->access->getConnection();
1211
		return $connection->getConnectionResource();
1212
	}
1213
1214
	/**
1215
	 * @throws ServerNotAvailableException
1216
	 */
1217
	public function getDisplayName(string $gid): string {
1218
		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1219
			return $this->groupPluginManager->getDisplayName($gid);
1220
		}
1221
1222
		$cacheKey = 'group_getDisplayName' . $gid;
1223
		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1224
			return $displayName;
1225
		}
1226
1227
		$displayName = $this->access->readAttribute(
1228
			$this->access->groupname2dn($gid),
1229
			$this->access->connection->ldapGroupDisplayName);
1230
1231
		if ($displayName && (count($displayName) > 0)) {
1232
			$displayName = $displayName[0];
1233
			$this->access->connection->writeToCache($cacheKey, $displayName);
1234
			return $displayName;
1235
		}
1236
1237
		return '';
1238
	}
1239
}
1240