Issues (2553)

apps/user_ldap/lib/User_LDAP.php (2 issues)

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Daniel Kesselberg <[email protected]>
9
 * @author Dominik Schmidt <[email protected]>
10
 * @author felixboehm <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Roger Szabo <[email protected]>
18
 * @author root <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Tom Needham <[email protected]>
21
 * @author Victor Dubiniuk <[email protected]>
22
 * @author Vinicius Cubas Brand <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
namespace OCA\User_LDAP;
40
41
use OC\ServerNotAvailableException;
42
use OC\User\Backend;
43
use OC\User\NoUserException;
44
use OCA\User_LDAP\Exceptions\NotOnLDAP;
45
use OCA\User_LDAP\User\OfflineUser;
46
use OCA\User_LDAP\User\User;
47
use OCP\IConfig;
48
use OCP\IUserBackend;
49
use OCP\IUserSession;
50
use OCP\Notification\IManager as INotificationManager;
51
use OCP\User\Backend\ICountMappedUsersBackend;
52
use OCP\User\Backend\ICountUsersBackend;
53
use OCP\UserInterface;
54
use Psr\Log\LoggerInterface;
55
56
class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ICountUsersBackend, ICountMappedUsersBackend {
57
	/** @var \OCP\IConfig */
58
	protected $ocConfig;
59
60
	/** @var INotificationManager */
61
	protected $notificationManager;
62
63
	/** @var UserPluginManager */
64
	protected $userPluginManager;
65
66
	/** @var LoggerInterface */
67
	protected $logger;
68
69
	/**
70
	 * @param Access $access
71
	 * @param \OCP\IConfig $ocConfig
72
	 * @param \OCP\Notification\IManager $notificationManager
73
	 * @param IUserSession $userSession
74
	 */
75
	public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, IUserSession $userSession, UserPluginManager $userPluginManager) {
0 ignored issues
show
The parameter $userSession 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

75
	public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, /** @scrutinizer ignore-unused */ IUserSession $userSession, UserPluginManager $userPluginManager) {

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...
76
		parent::__construct($access);
77
		$this->ocConfig = $ocConfig;
78
		$this->notificationManager = $notificationManager;
79
		$this->userPluginManager = $userPluginManager;
80
		$this->logger = \OC::$server->get(LoggerInterface::class);
81
	}
82
83
	/**
84
	 * checks whether the user is allowed to change his avatar in Nextcloud
85
	 *
86
	 * @param string $uid the Nextcloud user name
87
	 * @return boolean either the user can or cannot
88
	 * @throws \Exception
89
	 */
90
	public function canChangeAvatar($uid) {
91
		if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
92
			return $this->userPluginManager->canChangeAvatar($uid);
93
		}
94
95
		if (!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
96
			return true;
97
		}
98
99
		$user = $this->access->userManager->get($uid);
100
		if (!$user instanceof User) {
101
			return false;
102
		}
103
		$imageData = $user->getAvatarImage();
104
		if ($imageData === false) {
105
			return true;
106
		}
107
		return !$user->updateAvatar(true);
108
	}
109
110
	/**
111
	 * Return the username for the given login name, if available
112
	 *
113
	 * @param string $loginName
114
	 * @return string|false
115
	 * @throws \Exception
116
	 */
117
	public function loginName2UserName($loginName) {
118
		$cacheKey = 'loginName2UserName-' . $loginName;
119
		$username = $this->access->connection->getFromCache($cacheKey);
120
121
		if ($username !== null) {
122
			return $username;
123
		}
124
125
		try {
126
			$ldapRecord = $this->getLDAPUserByLoginName($loginName);
127
			$user = $this->access->userManager->get($ldapRecord['dn'][0]);
128
			if ($user === null || $user instanceof OfflineUser) {
129
				// this path is not really possible, however get() is documented
130
				// to return User, OfflineUser or null so we are very defensive here.
131
				$this->access->connection->writeToCache($cacheKey, false);
132
				return false;
133
			}
134
			$username = $user->getUsername();
135
			$this->access->connection->writeToCache($cacheKey, $username);
136
			return $username;
137
		} catch (NotOnLDAP $e) {
138
			$this->access->connection->writeToCache($cacheKey, false);
139
			return false;
140
		}
141
	}
142
143
	/**
144
	 * returns the username for the given LDAP DN, if available
145
	 *
146
	 * @param string $dn
147
	 * @return string|false with the username
148
	 */
149
	public function dn2UserName($dn) {
150
		return $this->access->dn2username($dn);
151
	}
152
153
	/**
154
	 * returns an LDAP record based on a given login name
155
	 *
156
	 * @param string $loginName
157
	 * @return array
158
	 * @throws NotOnLDAP
159
	 */
160
	public function getLDAPUserByLoginName($loginName) {
161
		//find out dn of the user name
162
		$attrs = $this->access->userManager->getAttributes();
163
		$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
164
		if (count($users) < 1) {
165
			throw new NotOnLDAP('No user available for the given login name on ' .
166
				$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
167
		}
168
		return $users[0];
169
	}
170
171
	/**
172
	 * Check if the password is correct without logging in the user
173
	 *
174
	 * @param string $uid The username
175
	 * @param string $password The password
176
	 * @return false|string
177
	 */
178
	public function checkPassword($uid, $password) {
179
		try {
180
			$ldapRecord = $this->getLDAPUserByLoginName($uid);
181
		} catch (NotOnLDAP $e) {
182
			$this->logger->debug(
183
				$e->getMessage(),
184
				['app' => 'user_ldap', 'exception' => $e]
185
			);
186
			return false;
187
		}
188
		$dn = $ldapRecord['dn'][0];
189
		$user = $this->access->userManager->get($dn);
190
191
		if (!$user instanceof User) {
192
			$this->logger->warning(
193
				'LDAP Login: Could not get user object for DN ' . $dn .
194
				'. Maybe the LDAP entry has no set display name attribute?',
195
				['app' => 'user_ldap']
196
			);
197
			return false;
198
		}
199
		if ($user->getUsername() !== false) {
200
			//are the credentials OK?
201
			if (!$this->access->areCredentialsValid($dn, $password)) {
202
				return false;
203
			}
204
205
			$this->access->cacheUserExists($user->getUsername());
206
			$user->processAttributes($ldapRecord);
207
			$user->markLogin();
208
209
			return $user->getUsername();
210
		}
211
212
		return false;
213
	}
214
215
	/**
216
	 * Set password
217
	 * @param string $uid The username
218
	 * @param string $password The new password
219
	 * @return bool
220
	 */
221
	public function setPassword($uid, $password) {
222
		if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
223
			return $this->userPluginManager->setPassword($uid, $password);
224
		}
225
226
		$user = $this->access->userManager->get($uid);
227
228
		if (!$user instanceof User) {
229
			throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
230
				'. Maybe the LDAP entry has no set display name attribute?');
231
		}
232
		if ($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
233
			$ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
234
			$turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
235
			if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
236
				//remove last password expiry warning if any
237
				$notification = $this->notificationManager->createNotification();
238
				$notification->setApp('user_ldap')
239
					->setUser($uid)
240
					->setObject('pwd_exp_warn', $uid)
241
				;
242
				$this->notificationManager->markProcessed($notification);
243
			}
244
			return true;
245
		}
246
247
		return false;
248
	}
249
250
	/**
251
	 * Get a list of all users
252
	 *
253
	 * @param string $search
254
	 * @param integer $limit
255
	 * @param integer $offset
256
	 * @return string[] an array of all uids
257
	 */
258
	public function getUsers($search = '', $limit = 10, $offset = 0) {
259
		$search = $this->access->escapeFilterPart($search, true);
260
		$cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
261
262
		//check if users are cached, if so return
263
		$ldap_users = $this->access->connection->getFromCache($cachekey);
264
		if (!is_null($ldap_users)) {
265
			return $ldap_users;
266
		}
267
268
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
269
		// error. With a limit of 0, we get 0 results. So we pass null.
270
		if ($limit <= 0) {
271
			$limit = null;
272
		}
273
		$filter = $this->access->combineFilterWithAnd([
274
			$this->access->connection->ldapUserFilter,
275
			$this->access->connection->ldapUserDisplayName . '=*',
276
			$this->access->getFilterPartForUserSearch($search)
277
		]);
278
279
		$this->logger->debug(
280
			'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
281
			['app' => 'user_ldap']
282
		);
283
		//do the search and translate results to Nextcloud names
284
		$ldap_users = $this->access->fetchListOfUsers(
285
			$filter,
286
			$this->access->userManager->getAttributes(true),
287
			$limit, $offset);
288
		$ldap_users = $this->access->nextcloudUserNames($ldap_users);
289
		$this->logger->debug(
290
			'getUsers: '.count($ldap_users). ' Users found',
291
			['app' => 'user_ldap']
292
		);
293
294
		$this->access->connection->writeToCache($cachekey, $ldap_users);
295
		return $ldap_users;
296
	}
297
298
	/**
299
	 * checks whether a user is still available on LDAP
300
	 *
301
	 * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
302
	 * name or an instance of that user
303
	 * @throws \Exception
304
	 * @throws \OC\ServerNotAvailableException
305
	 */
306
	public function userExistsOnLDAP($user, bool $ignoreCache = false): bool {
307
		if (is_string($user)) {
308
			$user = $this->access->userManager->get($user);
309
		}
310
		if (is_null($user)) {
311
			return false;
312
		}
313
		$uid = $user instanceof User ? $user->getUsername() : $user->getOCName();
314
		$cacheKey = 'userExistsOnLDAP' . $uid;
315
		if (!$ignoreCache) {
316
			$userExists = $this->access->connection->getFromCache($cacheKey);
317
			if (!is_null($userExists)) {
318
				return (bool)$userExists;
319
			}
320
		}
321
322
		$dn = $user->getDN();
323
		//check if user really still exists by reading its entry
324
		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
325
			try {
326
				$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
327
				if (!$uuid) {
328
					$this->access->connection->writeToCache($cacheKey, false);
329
					return false;
330
				}
331
				$newDn = $this->access->getUserDnByUuid($uuid);
332
				//check if renamed user is still valid by reapplying the ldap filter
333
				if ($newDn === $dn || !is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
334
					$this->access->connection->writeToCache($cacheKey, false);
335
					return false;
336
				}
337
				$this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
338
			} catch (ServerNotAvailableException $e) {
339
				throw $e;
340
			} catch (\Exception $e) {
341
				$this->access->connection->writeToCache($cacheKey, false);
342
				return false;
343
			}
344
		}
345
346
		if ($user instanceof OfflineUser) {
347
			$user->unmark();
348
		}
349
350
		$this->access->connection->writeToCache($cacheKey, true);
351
		return true;
352
	}
353
354
	/**
355
	 * check if a user exists
356
	 * @param string $uid the username
357
	 * @return boolean
358
	 * @throws \Exception when connection could not be established
359
	 */
360
	public function userExists($uid) {
361
		$userExists = $this->access->connection->getFromCache('userExists'.$uid);
362
		if (!is_null($userExists)) {
363
			return (bool)$userExists;
364
		}
365
		//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
366
		$user = $this->access->userManager->get($uid);
367
368
		if (is_null($user)) {
369
			$this->logger->debug(
370
				'No DN found for '.$uid.' on '.$this->access->connection->ldapHost,
371
				['app' => 'user_ldap']
372
			);
373
			$this->access->connection->writeToCache('userExists'.$uid, false);
374
			return false;
375
		}
376
377
		$this->access->connection->writeToCache('userExists'.$uid, true);
378
		return true;
379
	}
380
381
	/**
382
	 * returns whether a user was deleted in LDAP
383
	 *
384
	 * @param string $uid The username of the user to delete
385
	 * @return bool
386
	 */
387
	public function deleteUser($uid) {
388
		if ($this->userPluginManager->canDeleteUser()) {
389
			$status = $this->userPluginManager->deleteUser($uid);
390
			if ($status === false) {
391
				return false;
392
			}
393
		}
394
395
		$marked = (int)$this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
396
		if ($marked === 0) {
397
			try {
398
				$user = $this->access->userManager->get($uid);
399
				if (($user instanceof User) && !$this->userExistsOnLDAP($uid, true)) {
400
					$user->markUser();
401
					$marked = 1;
402
				}
403
			} catch (\Exception $e) {
404
				$this->logger->debug(
405
					$e->getMessage(),
406
					['app' => 'user_ldap', 'exception' => $e]
407
				);
408
			}
409
			if ($marked === 0) {
410
				$this->logger->notice(
411
					'User '.$uid . ' is not marked as deleted, not cleaning up.',
412
					['app' => 'user_ldap']
413
				);
414
				return false;
415
			}
416
		}
417
		$this->logger->info('Cleaning up after user ' . $uid,
418
			['app' => 'user_ldap']);
419
420
		$this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
421
		$this->access->userManager->invalidate($uid);
422
		$this->access->connection->clearCache();
423
		return true;
424
	}
425
426
	/**
427
	 * get the user's home directory
428
	 *
429
	 * @param string $uid the username
430
	 * @return bool|string
431
	 * @throws NoUserException
432
	 * @throws \Exception
433
	 */
434
	public function getHome($uid) {
435
		// user Exists check required as it is not done in user proxy!
436
		if (!$this->userExists($uid)) {
437
			return false;
438
		}
439
440
		if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
441
			return $this->userPluginManager->getHome($uid);
442
		}
443
444
		$cacheKey = 'getHome'.$uid;
445
		$path = $this->access->connection->getFromCache($cacheKey);
446
		if (!is_null($path)) {
447
			return $path;
448
		}
449
450
		// early return path if it is a deleted user
451
		$user = $this->access->userManager->get($uid);
452
		if ($user instanceof User || $user instanceof OfflineUser) {
453
			$path = $user->getHomePath() ?: false;
454
		} else {
455
			throw new NoUserException($uid . ' is not a valid user anymore');
456
		}
457
458
		$this->access->cacheUserHome($uid, $path);
459
		return $path;
460
	}
461
462
	/**
463
	 * get display name of the user
464
	 * @param string $uid user ID of the user
465
	 * @return string|false display name
466
	 */
467
	public function getDisplayName($uid) {
468
		if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
469
			return $this->userPluginManager->getDisplayName($uid);
470
		}
471
472
		if (!$this->userExists($uid)) {
473
			return false;
474
		}
475
476
		$cacheKey = 'getDisplayName'.$uid;
477
		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
478
			return $displayName;
479
		}
480
481
		//Check whether the display name is configured to have a 2nd feature
482
		$additionalAttribute = $this->access->connection->ldapUserDisplayName2;
483
		$displayName2 = '';
484
		if ($additionalAttribute !== '') {
485
			$displayName2 = $this->access->readAttribute(
486
				$this->access->username2dn($uid),
487
				$additionalAttribute);
488
		}
489
490
		$displayName = $this->access->readAttribute(
491
			$this->access->username2dn($uid),
492
			$this->access->connection->ldapUserDisplayName);
493
494
		if ($displayName && (count($displayName) > 0)) {
495
			$displayName = $displayName[0];
496
497
			if (is_array($displayName2)) {
498
				$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
499
			}
500
501
			$user = $this->access->userManager->get($uid);
502
			if ($user instanceof User) {
503
				$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
504
				$this->access->connection->writeToCache($cacheKey, $displayName);
505
			}
506
			if ($user instanceof OfflineUser) {
507
				/** @var OfflineUser $user*/
508
				$displayName = $user->getDisplayName();
509
			}
510
			return $displayName;
511
		}
512
513
		return null;
514
	}
515
516
	/**
517
	 * set display name of the user
518
	 * @param string $uid user ID of the user
519
	 * @param string $displayName new display name of the user
520
	 * @return string|false display name
521
	 */
522
	public function setDisplayName($uid, $displayName) {
523
		if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
524
			$this->userPluginManager->setDisplayName($uid, $displayName);
525
			$this->access->cacheUserDisplayName($uid, $displayName);
526
			return $displayName;
527
		}
528
		return false;
529
	}
530
531
	/**
532
	 * Get a list of all display names
533
	 *
534
	 * @param string $search
535
	 * @param int|null $limit
536
	 * @param int|null $offset
537
	 * @return array an array of all displayNames (value) and the corresponding uids (key)
538
	 */
539
	public function getDisplayNames($search = '', $limit = null, $offset = null) {
540
		$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
541
		if (!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
542
			return $displayNames;
543
		}
544
545
		$displayNames = [];
546
		$users = $this->getUsers($search, $limit, $offset);
547
		foreach ($users as $user) {
548
			$displayNames[$user] = $this->getDisplayName($user);
549
		}
550
		$this->access->connection->writeToCache($cacheKey, $displayNames);
551
		return $displayNames;
552
	}
553
554
	/**
555
	 * Check if backend implements actions
556
	 * @param int $actions bitwise-or'ed actions
557
	 * @return boolean
558
	 *
559
	 * Returns the supported actions as int to be
560
	 * compared with \OC\User\Backend::CREATE_USER etc.
561
	 */
562
	public function implementsActions($actions) {
563
		return (bool)((Backend::CHECK_PASSWORD
564
			| Backend::GET_HOME
565
			| Backend::GET_DISPLAYNAME
566
			| (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0)
567
			| Backend::COUNT_USERS
568
			| (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
569
			| $this->userPluginManager->getImplementedActions())
570
			& $actions);
571
	}
572
573
	/**
574
	 * @return bool
575
	 */
576
	public function hasUserListings() {
577
		return true;
578
	}
579
580
	/**
581
	 * counts the users in LDAP
582
	 *
583
	 * @return int|false
584
	 */
585
	public function countUsers() {
586
		if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
587
			return $this->userPluginManager->countUsers();
588
		}
589
590
		$filter = $this->access->getFilterForUserCount();
591
		$cacheKey = 'countUsers-'.$filter;
592
		if (!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
593
			return $entries;
594
		}
595
		$entries = $this->access->countUsers($filter);
596
		$this->access->connection->writeToCache($cacheKey, $entries);
597
		return $entries;
598
	}
599
600
	public function countMappedUsers(): int {
601
		return $this->access->getUserMapper()->count();
602
	}
603
604
	/**
605
	 * Backend name to be shown in user management
606
	 * @return string the name of the backend to be shown
607
	 */
608
	public function getBackendName() {
609
		return 'LDAP';
610
	}
611
612
	/**
613
	 * Return access for LDAP interaction.
614
	 * @param string $uid
615
	 * @return Access instance of Access for LDAP interaction
616
	 */
617
	public function getLDAPAccess($uid) {
618
		return $this->access;
619
	}
620
621
	/**
622
	 * Return LDAP connection resource from a cloned connection.
623
	 * The cloned connection needs to be closed manually.
624
	 * of the current access.
625
	 * @param string $uid
626
	 * @return resource|\LDAP\Connection The LDAP connection
627
	 */
628
	public function getNewLDAPConnection($uid) {
629
		$connection = clone $this->access->getConnection();
630
		return $connection->getConnectionResource();
631
	}
632
633
	/**
634
	 * create new user
635
	 * @param string $username username of the new user
636
	 * @param string $password password of the new user
637
	 * @throws \UnexpectedValueException
638
	 * @return bool
639
	 */
640
	public function createUser($username, $password) {
641
		if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
642
			if ($dn = $this->userPluginManager->createUser($username, $password)) {
643
				if (is_string($dn)) {
0 ignored issues
show
The condition is_string($dn) is always true.
Loading history...
644
					// the NC user creation work flow requires a know user id up front
645
					$uuid = $this->access->getUUID($dn, true);
646
					if (is_string($uuid)) {
647
						$this->access->mapAndAnnounceIfApplicable(
648
							$this->access->getUserMapper(),
649
							$dn,
650
							$username,
651
							$uuid,
652
							true
653
						);
654
						$this->access->cacheUserExists($username);
655
					} else {
656
						$this->logger->warning(
657
							'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
658
							[
659
								'app' => 'user_ldap',
660
								'userid' => $username,
661
							]
662
						);
663
					}
664
				} else {
665
					throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean.");
666
				}
667
			}
668
			return (bool) $dn;
669
		}
670
		return false;
671
	}
672
}
673