Passed
Push — master ( 675ec9...778ef8 )
by Roeland
09:42 queued 11s
created

Group_LDAP::getGroups()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 20
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 31
rs 9.6
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 OC\Cache\CappedMemoryCache;
47
use OCP\Group\Backend\IGetDisplayNameBackend;
48
use OCP\GroupInterface;
49
use OCP\ILogger;
50
51
class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
52
	protected $enabled = false;
53
54
	/**
55
	 * @var string[] $cachedGroupMembers array of users with gid as key
56
	 */
57
	protected $cachedGroupMembers;
58
59
	/**
60
	 * @var string[] $cachedGroupsByMember array of groups with uid as key
61
	 */
62
	protected $cachedGroupsByMember;
63
64
	/**
65
	 * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
66
	 */
67
	protected $cachedNestedGroups;
68
69
	/** @var GroupPluginManager */
70
	protected $groupPluginManager;
71
72
	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
73
		parent::__construct($access);
74
		$filter = $this->access->connection->ldapGroupFilter;
75
		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
76
		if (!empty($filter) && !empty($gassoc)) {
77
			$this->enabled = true;
78
		}
79
80
		$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...
81
		$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...
82
		$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...
83
		$this->groupPluginManager = $groupPluginManager;
84
	}
85
86
	/**
87
	 * is user in group?
88
	 *
89
	 * @param string $uid uid of the user
90
	 * @param string $gid gid of the group
91
	 * @return bool
92
	 *
93
	 * Checks whether the user is member of a group or not.
94
	 */
95
	public function inGroup($uid, $gid) {
96
		if (!$this->enabled) {
97
			return false;
98
		}
99
		$cacheKey = 'inGroup' . $uid . ':' . $gid;
100
		$inGroup = $this->access->connection->getFromCache($cacheKey);
101
		if (!is_null($inGroup)) {
102
			return (bool)$inGroup;
103
		}
104
105
		$userDN = $this->access->username2dn($uid);
106
107
		if (isset($this->cachedGroupMembers[$gid])) {
108
			$isInGroup = 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

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

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

209
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No search filter found on member url ' .

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

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

328
	public function gidNumber2Name($gid, /** @scrutinizer ignore-unused */ $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...
329
		$cacheKey = 'gidNumberToName' . $gid;
330
		$groupName = $this->access->connection->getFromCache($cacheKey);
331
		if (!is_null($groupName) && isset($groupName)) {
332
			return $groupName;
333
		}
334
335
		//we need to get the DN from LDAP
336
		$filter = $this->access->combineFilterWithAnd([
337
			$this->access->connection->ldapGroupFilter,
338
			'objectClass=posixGroup',
339
			$this->access->connection->ldapGidNumber . '=' . $gid
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGidNumber does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
340
		]);
341
		$result = $this->access->searchGroups($filter, ['dn'], 1);
342
		if (empty($result)) {
343
			return false;
344
		}
345
		$dn = $result[0]['dn'][0];
346
347
		//and now the group name
348
		//NOTE once we have separate ownCloud group IDs and group names we can
349
		//directly read the display name attribute instead of the DN
350
		$name = $this->access->dn2groupname($dn);
351
352
		$this->access->connection->writeToCache($cacheKey, $name);
353
354
		return $name;
355
	}
356
357
	/**
358
	 * returns the entry's gidNumber
359
	 *
360
	 * @param string $dn
361
	 * @param string $attribute
362
	 * @return string|bool
363
	 */
364
	private function getEntryGidNumber($dn, $attribute) {
365
		$value = $this->access->readAttribute($dn, $attribute);
366
		if (is_array($value) && !empty($value)) {
367
			return $value[0];
368
		}
369
		return false;
370
	}
371
372
	/**
373
	 * returns the group's primary ID
374
	 *
375
	 * @param string $dn
376
	 * @return string|bool
377
	 */
378
	public function getGroupGidNumber($dn) {
379
		return $this->getEntryGidNumber($dn, 'gidNumber');
380
	}
381
382
	/**
383
	 * returns the user's gidNumber
384
	 *
385
	 * @param string $dn
386
	 * @return string|bool
387
	 */
388
	public function getUserGidNumber($dn) {
389
		$gidNumber = false;
390
		if ($this->access->connection->hasGidNumber) {
391
			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGidNumber does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
392
			if ($gidNumber === false) {
393
				$this->access->connection->hasGidNumber = false;
394
			}
395
		}
396
		return $gidNumber;
397
	}
398
399
	/**
400
	 * returns a filter for a "users has specific gid" search or count operation
401
	 *
402
	 * @param string $groupDN
403
	 * @param string $search
404
	 * @return string
405
	 * @throws \Exception
406
	 */
407
	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
408
		$groupID = $this->getGroupGidNumber($groupDN);
409
		if ($groupID === false) {
410
			throw new \Exception('Not a valid group');
411
		}
412
413
		$filterParts = [];
414
		$filterParts[] = $this->access->getFilterForUserCount();
415
		if ($search !== '') {
416
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
417
		}
418
		$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

418
		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
419
420
		return $this->access->combineFilterWithAnd($filterParts);
421
	}
422
423
	/**
424
	 * returns a list of users that have the given group as gid number
425
	 *
426
	 * @param string $groupDN
427
	 * @param string $search
428
	 * @param int $limit
429
	 * @param int $offset
430
	 * @return string[]
431
	 */
432
	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
433
		try {
434
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
435
			$users = $this->access->fetchListOfUsers(
436
				$filter,
437
				[$this->access->connection->ldapUserDisplayName, 'dn'],
438
				$limit,
439
				$offset
440
			);
441
			return $this->access->nextcloudUserNames($users);
442
		} catch (\Exception $e) {
443
			return [];
444
		}
445
	}
446
447
	/**
448
	 * returns the number of users that have the given group as gid number
449
	 *
450
	 * @param string $groupDN
451
	 * @param string $search
452
	 * @param int $limit
453
	 * @param int $offset
454
	 * @return int
455
	 */
456
	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
457
		try {
458
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
459
			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
460
			return (int)$users;
461
		} catch (\Exception $e) {
462
			return 0;
463
		}
464
	}
465
466
	/**
467
	 * gets the gidNumber of a user
468
	 *
469
	 * @param string $dn
470
	 * @return string
471
	 */
472
	public function getUserGroupByGid($dn) {
473
		$groupID = $this->getUserGidNumber($dn);
474
		if ($groupID !== false) {
475
			$groupName = $this->gidNumber2Name($groupID, $dn);
476
			if ($groupName !== false) {
477
				return $groupName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $groupName also could return the type true which is incompatible with the documented return type string.
Loading history...
478
			}
479
		}
480
481
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
482
	}
483
484
	/**
485
	 * translates a primary group ID into an Nextcloud internal name
486
	 *
487
	 * @param string $gid as given by primaryGroupID on AD
488
	 * @param string $dn a DN that belongs to the same domain as the group
489
	 * @return string|bool
490
	 */
491
	public function primaryGroupID2Name($gid, $dn) {
492
		$cacheKey = 'primaryGroupIDtoName';
493
		$groupNames = $this->access->connection->getFromCache($cacheKey);
494
		if (!is_null($groupNames) && isset($groupNames[$gid])) {
495
			return $groupNames[$gid];
496
		}
497
498
		$domainObjectSid = $this->access->getSID($dn);
499
		if ($domainObjectSid === false) {
500
			return false;
501
		}
502
503
		//we need to get the DN from LDAP
504
		$filter = $this->access->combineFilterWithAnd([
505
			$this->access->connection->ldapGroupFilter,
506
			'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

506
			'objectsid=' . /** @scrutinizer ignore-type */ $domainObjectSid . '-' . $gid
Loading history...
507
		]);
508
		$result = $this->access->searchGroups($filter, ['dn'], 1);
509
		if (empty($result)) {
510
			return false;
511
		}
512
		$dn = $result[0]['dn'][0];
513
514
		//and now the group name
515
		//NOTE once we have separate Nextcloud group IDs and group names we can
516
		//directly read the display name attribute instead of the DN
517
		$name = $this->access->dn2groupname($dn);
518
519
		$this->access->connection->writeToCache($cacheKey, $name);
520
521
		return $name;
522
	}
523
524
	/**
525
	 * returns the entry's primary group ID
526
	 *
527
	 * @param string $dn
528
	 * @param string $attribute
529
	 * @return string|bool
530
	 */
531
	private function getEntryGroupID($dn, $attribute) {
532
		$value = $this->access->readAttribute($dn, $attribute);
533
		if (is_array($value) && !empty($value)) {
534
			return $value[0];
535
		}
536
		return false;
537
	}
538
539
	/**
540
	 * returns the group's primary ID
541
	 *
542
	 * @param string $dn
543
	 * @return string|bool
544
	 */
545
	public function getGroupPrimaryGroupID($dn) {
546
		return $this->getEntryGroupID($dn, 'primaryGroupToken');
547
	}
548
549
	/**
550
	 * returns the user's primary group ID
551
	 *
552
	 * @param string $dn
553
	 * @return string|bool
554
	 */
555
	public function getUserPrimaryGroupIDs($dn) {
556
		$primaryGroupID = false;
557
		if ($this->access->connection->hasPrimaryGroups) {
558
			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
559
			if ($primaryGroupID === false) {
560
				$this->access->connection->hasPrimaryGroups = false;
561
			}
562
		}
563
		return $primaryGroupID;
564
	}
565
566
	/**
567
	 * returns a filter for a "users in primary group" search or count operation
568
	 *
569
	 * @param string $groupDN
570
	 * @param string $search
571
	 * @return string
572
	 * @throws \Exception
573
	 */
574
	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
575
		$groupID = $this->getGroupPrimaryGroupID($groupDN);
576
		if ($groupID === false) {
577
			throw new \Exception('Not a valid group');
578
		}
579
580
		$filterParts = [];
581
		$filterParts[] = $this->access->getFilterForUserCount();
582
		if ($search !== '') {
583
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
584
		}
585
		$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

585
		$filterParts[] = 'primaryGroupID=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
586
587
		return $this->access->combineFilterWithAnd($filterParts);
588
	}
589
590
	/**
591
	 * returns a list of users that have the given group as primary group
592
	 *
593
	 * @param string $groupDN
594
	 * @param string $search
595
	 * @param int $limit
596
	 * @param int $offset
597
	 * @return string[]
598
	 */
599
	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
600
		try {
601
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
602
			$users = $this->access->fetchListOfUsers(
603
				$filter,
604
				[$this->access->connection->ldapUserDisplayName, 'dn'],
605
				$limit,
606
				$offset
607
			);
608
			return $this->access->nextcloudUserNames($users);
609
		} catch (\Exception $e) {
610
			return [];
611
		}
612
	}
613
614
	/**
615
	 * returns the number of users that have the given group as primary group
616
	 *
617
	 * @param string $groupDN
618
	 * @param string $search
619
	 * @param int $limit
620
	 * @param int $offset
621
	 * @return int
622
	 */
623
	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
624
		try {
625
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
626
			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
627
			return (int)$users;
628
		} catch (\Exception $e) {
629
			return 0;
630
		}
631
	}
632
633
	/**
634
	 * gets the primary group of a user
635
	 *
636
	 * @param string $dn
637
	 * @return string
638
	 */
639
	public function getUserPrimaryGroup($dn) {
640
		$groupID = $this->getUserPrimaryGroupIDs($dn);
641
		if ($groupID !== false) {
642
			$groupName = $this->primaryGroupID2Name($groupID, $dn);
643
			if ($groupName !== false) {
644
				return $groupName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $groupName also could return the type true which is incompatible with the documented return type string.
Loading history...
645
			}
646
		}
647
648
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
649
	}
650
651
	/**
652
	 * Get all groups a user belongs to
653
	 *
654
	 * @param string $uid Name of the user
655
	 * @return array with group names
656
	 *
657
	 * This function fetches all groups a user belongs to. It does not check
658
	 * if the user exists at all.
659
	 *
660
	 * This function includes groups based on dynamic group membership.
661
	 */
662
	public function getUserGroups($uid) {
663
		if (!$this->enabled) {
664
			return [];
665
		}
666
		$cacheKey = 'getUserGroups' . $uid;
667
		$userGroups = $this->access->connection->getFromCache($cacheKey);
668
		if (!is_null($userGroups)) {
669
			return $userGroups;
670
		}
671
		$userDN = $this->access->username2dn($uid);
672
		if (!$userDN) {
673
			$this->access->connection->writeToCache($cacheKey, []);
674
			return [];
675
		}
676
677
		$groups = [];
678
		$primaryGroup = $this->getUserPrimaryGroup($userDN);
679
		$gidGroupName = $this->getUserGroupByGid($userDN);
680
681
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapDynamicGroupMemberURL does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
682
683
		if (!empty($dynamicGroupMemberURL)) {
684
			// look through dynamic groups to add them to the result array if needed
685
			$groupsToMatch = $this->access->fetchListOfGroups(
686
				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
687
			foreach ($groupsToMatch as $dynamicGroup) {
688
				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
689
					continue;
690
				}
691
				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
692
				if ($pos !== false) {
693
					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
694
					// apply filter via ldap search to see if this user is in this
695
					// dynamic group
696
					$userMatch = $this->access->readAttribute(
697
						$userDN,
698
						$this->access->connection->ldapUserDisplayName,
699
						$memberUrlFilter
700
					);
701
					if ($userMatch !== false) {
702
						// match found so this user is in this group
703
						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
704
						if (is_string($groupName)) {
705
							// be sure to never return false if the dn could not be
706
							// resolved to a name, for whatever reason.
707
							$groups[] = $groupName;
708
						}
709
					}
710
				} else {
711
					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url ' .
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

711
					/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No search filter found on member url ' .

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
712
						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
713
				}
714
			}
715
		}
716
717
		// if possible, read out membership via memberOf. It's far faster than
718
		// performing a search, which still is a fallback later.
719
		// memberof doesn't support memberuid, so skip it here.
720
		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
0 ignored issues
show
Bug Best Practice introduced by
The property hasMemberOfFilterSupport does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
721
			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
0 ignored issues
show
Bug Best Practice introduced by
The property useMemberOfToDetectMembership does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
722
			&& strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
723
		) {
724
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
725
			if (is_array($groupDNs)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
726
				foreach ($groupDNs as $dn) {
727
					$groupName = $this->access->dn2groupname($dn);
728
					if (is_string($groupName)) {
729
						// be sure to never return false if the dn could not be
730
						// resolved to a name, for whatever reason.
731
						$groups[] = $groupName;
732
					}
733
				}
734
			}
735
736
			if ($primaryGroup !== false) {
0 ignored issues
show
introduced by
The condition $primaryGroup !== false is always true.
Loading history...
737
				$groups[] = $primaryGroup;
738
			}
739
			if ($gidGroupName !== false) {
0 ignored issues
show
introduced by
The condition $gidGroupName !== false is always true.
Loading history...
740
				$groups[] = $gidGroupName;
741
			}
742
			$this->access->connection->writeToCache($cacheKey, $groups);
743
			return $groups;
744
		}
745
746
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
747
		if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
748
			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
749
		) {
750
			$uid = $userDN;
751
		} elseif (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
752
			$result = $this->access->readAttribute($userDN, 'uid');
753
			if ($result === false) {
754
				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on ' .
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

754
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on ' .

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
755
					$this->access->connection->ldapHost, ILogger::DEBUG);
756
				$uid = false;
757
			} else {
758
				$uid = $result[0];
759
			}
760
		} else {
761
			// just in case
762
			$uid = $userDN;
763
		}
764
765
		if ($uid !== false) {
766
			if (isset($this->cachedGroupsByMember[$uid])) {
767
				$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

767
				$groups = array_merge($groups, /** @scrutinizer ignore-type */ $this->cachedGroupsByMember[$uid]);
Loading history...
768
			} else {
769
				$groupsByMember = array_values($this->getGroupsByMember($uid));
770
				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
771
				$this->cachedGroupsByMember[$uid] = $groupsByMember;
772
				$groups = array_merge($groups, $groupsByMember);
773
			}
774
		}
775
776
		if ($primaryGroup !== false) {
0 ignored issues
show
introduced by
The condition $primaryGroup !== false is always true.
Loading history...
777
			$groups[] = $primaryGroup;
778
		}
779
		if ($gidGroupName !== false) {
0 ignored issues
show
introduced by
The condition $gidGroupName !== false is always true.
Loading history...
780
			$groups[] = $gidGroupName;
781
		}
782
783
		$groups = array_unique($groups, SORT_LOCALE_STRING);
784
		$this->access->connection->writeToCache($cacheKey, $groups);
785
786
		return $groups;
787
	}
788
789
	/**
790
	 * @param string $dn
791
	 * @param array|null &$seen
792
	 * @return array
793
	 */
794
	private function getGroupsByMember($dn, &$seen = null) {
795
		if ($seen === null) {
796
			$seen = [];
797
		}
798
		if (array_key_exists($dn, $seen)) {
799
			// avoid loops
800
			return [];
801
		}
802
		$allGroups = [];
803
		$seen[$dn] = true;
804
		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
805
		$groups = $this->access->fetchListOfGroups($filter,
806
			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
807
		if (is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
808
			$fetcher = function ($dn, &$seen) {
809
				if (is_array($dn) && isset($dn['dn'][0])) {
810
					$dn = $dn['dn'][0];
811
				}
812
				return $this->getGroupsByMember($dn, $seen);
813
			};
814
			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
815
		}
816
		$visibleGroups = $this->filterValidGroups($allGroups);
817
		return array_intersect_key($allGroups, $visibleGroups);
818
	}
819
820
	/**
821
	 * get a list of all users in a group
822
	 *
823
	 * @param string $gid
824
	 * @param string $search
825
	 * @param int $limit
826
	 * @param int $offset
827
	 * @return array with user ids
828
	 * @throws \Exception
829
	 */
830
	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
831
		if (!$this->enabled) {
832
			return [];
833
		}
834
		if (!$this->groupExists($gid)) {
835
			return [];
836
		}
837
		$search = $this->access->escapeFilterPart($search, true);
838
		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
839
		// check for cache of the exact query
840
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
841
		if (!is_null($groupUsers)) {
842
			return $groupUsers;
843
		}
844
845
		if ($limit === -1) {
846
			$limit = null;
847
		}
848
		// check for cache of the query without limit and offset
849
		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
850
		if (!is_null($groupUsers)) {
851
			$groupUsers = array_slice($groupUsers, $offset, $limit);
852
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
853
			return $groupUsers;
854
		}
855
856
		$groupDN = $this->access->groupname2dn($gid);
857
		if (!$groupDN) {
858
			// group couldn't be found, return empty resultset
859
			$this->access->connection->writeToCache($cacheKey, []);
860
			return [];
861
		}
862
863
		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
864
		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
865
		$members = $this->_groupMembers($groupDN);
866
		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
867
			//in case users could not be retrieved, return empty result set
868
			$this->access->connection->writeToCache($cacheKey, []);
869
			return [];
870
		}
871
872
		$groupUsers = [];
873
		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
874
		$attrs = $this->access->userManager->getAttributes(true);
875
		foreach ($members as $member) {
876
			if ($isMemberUid) {
877
				//we got uids, need to get their DNs to 'translate' them to user names
878
				$filter = $this->access->combineFilterWithAnd([
879
					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
880
					$this->access->combineFilterWithAnd([
881
						$this->access->getFilterPartForUserSearch($search),
882
						$this->access->connection->ldapUserFilter
883
					])
884
				]);
885
				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
886
				if (count($ldap_users) < 1) {
887
					continue;
888
				}
889
				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
890
			} else {
891
				//we got DNs, check if we need to filter by search or we can give back all of them
892
				$uid = $this->access->dn2username($member);
893
				if (!$uid) {
894
					continue;
895
				}
896
897
				$cacheKey = 'userExistsOnLDAP' . $uid;
898
				$userExists = $this->access->connection->getFromCache($cacheKey);
899
				if ($userExists === false) {
900
					continue;
901
				}
902
				if ($userExists === null || $search !== '') {
903
					if (!$this->access->readAttribute($member,
904
						$this->access->connection->ldapUserDisplayName,
905
						$this->access->combineFilterWithAnd([
906
							$this->access->getFilterPartForUserSearch($search),
907
							$this->access->connection->ldapUserFilter
908
						]))) {
909
						if ($search === '') {
910
							$this->access->connection->writeToCache($cacheKey, false);
911
						}
912
						continue;
913
					}
914
					$this->access->connection->writeToCache($cacheKey, true);
915
				}
916
				$groupUsers[] = $uid;
917
			}
918
		}
919
920
		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
921
		natsort($groupUsers);
922
		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
923
		$groupUsers = array_slice($groupUsers, $offset, $limit);
924
925
		$this->access->connection->writeToCache($cacheKey, $groupUsers);
926
927
		return $groupUsers;
928
	}
929
930
	/**
931
	 * returns the number of users in a group, who match the search term
932
	 *
933
	 * @param string $gid the internal group name
934
	 * @param string $search optional, a search string
935
	 * @return int|bool
936
	 */
937
	public function countUsersInGroup($gid, $search = '') {
938
		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
939
			return $this->groupPluginManager->countUsersInGroup($gid, $search);
940
		}
941
942
		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
943
		if (!$this->enabled || !$this->groupExists($gid)) {
944
			return false;
945
		}
946
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
947
		if (!is_null($groupUsers)) {
948
			return $groupUsers;
949
		}
950
951
		$groupDN = $this->access->groupname2dn($gid);
952
		if (!$groupDN) {
953
			// group couldn't be found, return empty result set
954
			$this->access->connection->writeToCache($cacheKey, false);
955
			return false;
956
		}
957
958
		$members = $this->_groupMembers($groupDN);
959
		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
960
		if (!$members && $primaryUserCount === 0) {
961
			//in case users could not be retrieved, return empty result set
962
			$this->access->connection->writeToCache($cacheKey, false);
963
			return false;
964
		}
965
966
		if ($search === '') {
967
			$groupUsers = count($members) + $primaryUserCount;
968
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
969
			return $groupUsers;
970
		}
971
		$search = $this->access->escapeFilterPart($search, true);
972
		$isMemberUid =
973
			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
974
				=== 'memberuid');
975
976
		//we need to apply the search filter
977
		//alternatives that need to be checked:
978
		//a) get all users by search filter and array_intersect them
979
		//b) a, but only when less than 1k 10k ?k users like it is
980
		//c) put all DNs|uids in a LDAP filter, combine with the search string
981
		//   and let it count.
982
		//For now this is not important, because the only use of this method
983
		//does not supply a search string
984
		$groupUsers = [];
985
		foreach ($members as $member) {
986
			if ($isMemberUid) {
987
				//we got uids, need to get their DNs to 'translate' them to user names
988
				$filter = $this->access->combineFilterWithAnd([
989
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
990
					$this->access->getFilterPartForUserSearch($search)
991
				]);
992
				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
993
				if (count($ldap_users) < 1) {
994
					continue;
995
				}
996
				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
997
			} else {
998
				//we need to apply the search filter now
999
				if (!$this->access->readAttribute($member,
1000
					$this->access->connection->ldapUserDisplayName,
1001
					$this->access->getFilterPartForUserSearch($search))) {
1002
					continue;
1003
				}
1004
				// dn2username will also check if the users belong to the allowed base
1005
				if ($ocname = $this->access->dn2username($member)) {
1006
					$groupUsers[] = $ocname;
1007
				}
1008
			}
1009
		}
1010
1011
		//and get users that have the group as primary
1012
		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1013
1014
		return count($groupUsers) + $primaryUsers;
1015
	}
1016
1017
	/**
1018
	 * get a list of all groups using a paged search
1019
	 *
1020
	 * @param string $search
1021
	 * @param int $limit
1022
	 * @param int $offset
1023
	 * @return array with group names
1024
	 *
1025
	 * Returns a list with all groups
1026
	 * Uses a paged search if available to override a
1027
	 * server side search limit.
1028
	 * (active directory has a limit of 1000 by default)
1029
	 */
1030
	public function getGroups($search = '', $limit = -1, $offset = 0) {
1031
		if (!$this->enabled) {
1032
			return [];
1033
		}
1034
		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1035
1036
		//Check cache before driving unnecessary searches
1037
		\OCP\Util::writeLog('user_ldap', 'getGroups ' . $cacheKey, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

1037
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'getGroups ' . $cacheKey, ILogger::DEBUG);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1038
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1039
		if (!is_null($ldap_groups)) {
1040
			return $ldap_groups;
1041
		}
1042
1043
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1044
		// error. With a limit of 0, we get 0 results. So we pass null.
1045
		if ($limit <= 0) {
1046
			$limit = null;
1047
		}
1048
		$filter = $this->access->combineFilterWithAnd([
1049
			$this->access->connection->ldapGroupFilter,
1050
			$this->access->getFilterPartForGroupSearch($search)
1051
		]);
1052
		\OCP\Util::writeLog('user_ldap', 'getGroups Filter ' . $filter, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

1052
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'getGroups Filter ' . $filter, ILogger::DEBUG);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1053
		$ldap_groups = $this->access->fetchListOfGroups($filter,
1054
			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1055
			$limit,
1056
			$offset);
1057
		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1058
1059
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1060
		return $ldap_groups;
1061
	}
1062
1063
	/**
1064
	 * @param string $group
1065
	 * @return bool
1066
	 */
1067
	public function groupMatchesFilter($group) {
1068
		return (strripos($group, $this->groupSearch) !== false);
0 ignored issues
show
Bug Best Practice introduced by
The property groupSearch does not exist on OCA\User_LDAP\Group_LDAP. Did you maybe forget to declare it?
Loading history...
1069
	}
1070
1071
	/**
1072
	 * check if a group exists
1073
	 *
1074
	 * @param string $gid
1075
	 * @return bool
1076
	 */
1077
	public function groupExists($gid) {
1078
		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1079
		if (!is_null($groupExists)) {
1080
			return (bool)$groupExists;
1081
		}
1082
1083
		//getting dn, if false the group does not exist. If dn, it may be mapped
1084
		//only, requires more checking.
1085
		$dn = $this->access->groupname2dn($gid);
1086
		if (!$dn) {
1087
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1088
			return false;
1089
		}
1090
1091
		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1092
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1093
			return false;
1094
		}
1095
1096
		//if group really still exists, we will be able to read its objectclass
1097
		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1098
			$this->access->connection->writeToCache('groupExists' . $gid, false);
1099
			return false;
1100
		}
1101
1102
		$this->access->connection->writeToCache('groupExists' . $gid, true);
1103
		return true;
1104
	}
1105
1106
	protected function filterValidGroups(array $listOfGroups): array {
1107
		$validGroupDNs = [];
1108
		foreach ($listOfGroups as $key => $item) {
1109
			$dn = is_string($item) ? $item : $item['dn'][0];
1110
			$gid = $this->access->dn2groupname($dn);
1111
			if (!$gid) {
1112
				continue;
1113
			}
1114
			if ($this->groupExists($gid)) {
1115
				$validGroupDNs[$key] = $item;
1116
			}
1117
		}
1118
		return $validGroupDNs;
1119
	}
1120
1121
	/**
1122
	 * Check if backend implements actions
1123
	 *
1124
	 * @param int $actions bitwise-or'ed actions
1125
	 * @return boolean
1126
	 *
1127
	 * Returns the supported actions as int to be
1128
	 * compared with GroupInterface::CREATE_GROUP etc.
1129
	 */
1130
	public function implementsActions($actions) {
1131
		return (bool)((GroupInterface::COUNT_USERS |
1132
				$this->groupPluginManager->getImplementedActions()) & $actions);
1133
	}
1134
1135
	/**
1136
	 * Return access for LDAP interaction.
1137
	 *
1138
	 * @return Access instance of Access for LDAP interaction
1139
	 */
1140
	public function getLDAPAccess($gid) {
1141
		return $this->access;
1142
	}
1143
1144
	/**
1145
	 * create a group
1146
	 *
1147
	 * @param string $gid
1148
	 * @return bool
1149
	 * @throws \Exception
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
	 */
1251
	public function getNewLDAPConnection($gid) {
1252
		$connection = clone $this->access->getConnection();
1253
		return $connection->getConnectionResource();
1254
	}
1255
1256
	/**
1257
	 * @throws \OC\ServerNotAvailableException
1258
	 */
1259
	public function getDisplayName(string $gid): string {
1260
		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1261
			return $this->groupPluginManager->getDisplayName($gid);
1262
		}
1263
1264
		$cacheKey = 'group_getDisplayName' . $gid;
1265
		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1266
			return $displayName;
1267
		}
1268
1269
		$displayName = $this->access->readAttribute(
1270
			$this->access->groupname2dn($gid),
1271
			$this->access->connection->ldapGroupDisplayName);
1272
1273
		if ($displayName && (count($displayName) > 0)) {
1274
			$displayName = $displayName[0];
1275
			$this->access->connection->writeToCache($cacheKey, $displayName);
1276
			return $displayName;
1277
		}
1278
1279
		return '';
1280
	}
1281
}
1282