diff options
Diffstat (limited to 'apps/user_ldap/lib/User_LDAP.php')
-rw-r--r-- | apps/user_ldap/lib/User_LDAP.php | 635 |
1 files changed, 635 insertions, 0 deletions
diff --git a/apps/user_ldap/lib/User_LDAP.php b/apps/user_ldap/lib/User_LDAP.php new file mode 100644 index 00000000000..c3f56f5ff9b --- /dev/null +++ b/apps/user_ldap/lib/User_LDAP.php @@ -0,0 +1,635 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\User_LDAP; + +use OC\ServerNotAvailableException; +use OC\User\Backend; +use OC\User\NoUserException; +use OCA\User_LDAP\Exceptions\NotOnLDAP; +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCA\User_LDAP\User\OfflineUser; +use OCA\User_LDAP\User\User; +use OCP\IUserBackend; +use OCP\Notification\IManager as INotificationManager; +use OCP\User\Backend\ICountMappedUsersBackend; +use OCP\User\Backend\ILimitAwareCountUsersBackend; +use OCP\User\Backend\IProvideEnabledStateBackend; +use OCP\UserInterface; +use Psr\Log\LoggerInterface; + +class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend { + public function __construct( + Access $access, + protected INotificationManager $notificationManager, + protected UserPluginManager $userPluginManager, + protected LoggerInterface $logger, + protected DeletedUsersIndex $deletedUsersIndex, + ) { + parent::__construct($access); + } + + /** + * checks whether the user is allowed to change their avatar in Nextcloud + * + * @param string $uid the Nextcloud user name + * @return boolean either the user can or cannot + * @throws \Exception + */ + public function canChangeAvatar($uid) { + if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) { + return $this->userPluginManager->canChangeAvatar($uid); + } + + if (!$this->implementsActions(Backend::PROVIDE_AVATAR)) { + return true; + } + + $user = $this->access->userManager->get($uid); + if (!$user instanceof User) { + return false; + } + $imageData = $user->getAvatarImage(); + if ($imageData === false) { + return true; + } + return !$user->updateAvatar(true); + } + + /** + * Return the username for the given login name, if available + * + * @param string $loginName + * @return string|false + * @throws \Exception + */ + public function loginName2UserName($loginName, bool $forceLdapRefetch = false) { + $cacheKey = 'loginName2UserName-' . $loginName; + $username = $this->access->connection->getFromCache($cacheKey); + + $ignoreCache = ($username === false && $forceLdapRefetch); + if ($username !== null && !$ignoreCache) { + return $username; + } + + try { + $ldapRecord = $this->getLDAPUserByLoginName($loginName); + $user = $this->access->userManager->get($ldapRecord['dn'][0]); + if ($user === null || $user instanceof OfflineUser) { + // this path is not really possible, however get() is documented + // to return User, OfflineUser or null so we are very defensive here. + $this->access->connection->writeToCache($cacheKey, false); + return false; + } + $username = $user->getUsername(); + $this->access->connection->writeToCache($cacheKey, $username); + if ($forceLdapRefetch) { + $user->processAttributes($ldapRecord); + } + return $username; + } catch (NotOnLDAP $e) { + $this->access->connection->writeToCache($cacheKey, false); + return false; + } + } + + /** + * returns the username for the given LDAP DN, if available + * + * @param string $dn + * @return string|false with the username + */ + public function dn2UserName($dn) { + return $this->access->dn2username($dn); + } + + /** + * returns an LDAP record based on a given login name + * + * @param string $loginName + * @return array + * @throws NotOnLDAP + */ + public function getLDAPUserByLoginName($loginName) { + //find out dn of the user name + $attrs = $this->access->userManager->getAttributes(); + $users = $this->access->fetchUsersByLoginName($loginName, $attrs); + if (count($users) < 1) { + throw new NotOnLDAP('No user available for the given login name on ' + . $this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort); + } + return $users[0]; + } + + /** + * Check if the password is correct without logging in the user + * + * @param string $uid The username + * @param string $password The password + * @return false|string + */ + public function checkPassword($uid, $password) { + $username = $this->loginName2UserName($uid, true); + if ($username === false) { + return false; + } + $dn = $this->access->username2dn($username); + $user = $this->access->userManager->get($dn); + + if (!$user instanceof User) { + $this->logger->warning( + 'LDAP Login: Could not get user object for DN ' . $dn + . '. Maybe the LDAP entry has no set display name attribute?', + ['app' => 'user_ldap'] + ); + return false; + } + if ($user->getUsername() !== false) { + //are the credentials OK? + if (!$this->access->areCredentialsValid($dn, $password)) { + return false; + } + + $this->access->cacheUserExists($user->getUsername()); + $user->markLogin(); + + return $user->getUsername(); + } + + return false; + } + + /** + * Set password + * @param string $uid The username + * @param string $password The new password + * @return bool + */ + public function setPassword($uid, $password) { + if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) { + return $this->userPluginManager->setPassword($uid, $password); + } + + $user = $this->access->userManager->get($uid); + + if (!$user instanceof User) { + throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid + . '. Maybe the LDAP entry has no set display name attribute?'); + } + if ($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) { + $ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN; + $turnOnPasswordChange = $this->access->connection->turnOnPasswordChange; + if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) { + //remove last password expiry warning if any + $notification = $this->notificationManager->createNotification(); + $notification->setApp('user_ldap') + ->setUser($uid) + ->setObject('pwd_exp_warn', $uid) + ; + $this->notificationManager->markProcessed($notification); + } + return true; + } + + return false; + } + + /** + * Get a list of all users + * + * @param string $search + * @param integer $limit + * @param integer $offset + * @return string[] an array of all uids + */ + public function getUsers($search = '', $limit = 10, $offset = 0) { + $search = $this->access->escapeFilterPart($search, true); + $cachekey = 'getUsers-' . $search . '-' . $limit . '-' . $offset; + + //check if users are cached, if so return + $ldap_users = $this->access->connection->getFromCache($cachekey); + if (!is_null($ldap_users)) { + return $ldap_users; + } + + // if we'd pass -1 to LDAP search, we'd end up in a Protocol + // error. With a limit of 0, we get 0 results. So we pass null. + if ($limit <= 0) { + $limit = null; + } + $filter = $this->access->combineFilterWithAnd([ + $this->access->connection->ldapUserFilter, + $this->access->connection->ldapUserDisplayName . '=*', + $this->access->getFilterPartForUserSearch($search) + ]); + + $this->logger->debug( + 'getUsers: Options: search ' . $search . ' limit ' . $limit . ' offset ' . $offset . ' Filter: ' . $filter, + ['app' => 'user_ldap'] + ); + //do the search and translate results to Nextcloud names + $ldap_users = $this->access->fetchListOfUsers( + $filter, + $this->access->userManager->getAttributes(true), + $limit, $offset); + $ldap_users = $this->access->nextcloudUserNames($ldap_users); + $this->logger->debug( + 'getUsers: ' . count($ldap_users) . ' Users found', + ['app' => 'user_ldap'] + ); + + $this->access->connection->writeToCache($cachekey, $ldap_users); + return $ldap_users; + } + + /** + * checks whether a user is still available on LDAP + * + * @param string|User $user either the Nextcloud user + * name or an instance of that user + * @throws \Exception + * @throws \OC\ServerNotAvailableException + */ + public function userExistsOnLDAP($user, bool $ignoreCache = false): bool { + if (is_string($user)) { + $user = $this->access->userManager->get($user); + } + if (is_null($user)) { + return false; + } + $uid = $user instanceof User ? $user->getUsername() : $user->getOCName(); + $cacheKey = 'userExistsOnLDAP' . $uid; + if (!$ignoreCache) { + $userExists = $this->access->connection->getFromCache($cacheKey); + if (!is_null($userExists)) { + return (bool)$userExists; + } + } + + $dn = $user->getDN(); + //check if user really still exists by reading its entry + if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) { + try { + $uuid = $this->access->getUserMapper()->getUUIDByDN($dn); + if (!$uuid) { + $this->access->connection->writeToCache($cacheKey, false); + return false; + } + $newDn = $this->access->getUserDnByUuid($uuid); + //check if renamed user is still valid by reapplying the ldap filter + if ($newDn === $dn || !is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) { + $this->access->connection->writeToCache($cacheKey, false); + return false; + } + $this->access->getUserMapper()->setDNbyUUID($newDn, $uuid); + } catch (ServerNotAvailableException $e) { + throw $e; + } catch (\Exception $e) { + $this->access->connection->writeToCache($cacheKey, false); + return false; + } + } + + if ($user instanceof OfflineUser) { + $user->unmark(); + } + + $this->access->connection->writeToCache($cacheKey, true); + return true; + } + + /** + * check if a user exists + * @param string $uid the username + * @return boolean + * @throws \Exception when connection could not be established + */ + public function userExists($uid) { + $userExists = $this->access->connection->getFromCache('userExists' . $uid); + if (!is_null($userExists)) { + return (bool)$userExists; + } + $userExists = $this->access->userManager->exists($uid); + + if (!$userExists) { + $this->logger->debug( + 'No DN found for ' . $uid . ' on ' . $this->access->connection->ldapHost, + ['app' => 'user_ldap'] + ); + $this->access->connection->writeToCache('userExists' . $uid, false); + return false; + } + + $this->access->connection->writeToCache('userExists' . $uid, true); + return true; + } + + /** + * returns whether a user was deleted in LDAP + * + * @param string $uid The username of the user to delete + * @return bool + */ + public function deleteUser($uid) { + if ($this->userPluginManager->canDeleteUser()) { + $status = $this->userPluginManager->deleteUser($uid); + if ($status === false) { + return false; + } + } + + $marked = $this->deletedUsersIndex->isUserMarked($uid); + if (!$marked) { + try { + $user = $this->access->userManager->get($uid); + if (($user instanceof User) && !$this->userExistsOnLDAP($uid, true)) { + $user->markUser(); + $marked = true; + } + } catch (\Exception $e) { + $this->logger->debug( + $e->getMessage(), + ['app' => 'user_ldap', 'exception' => $e] + ); + } + if (!$marked) { + $this->logger->notice( + 'User ' . $uid . ' is not marked as deleted, not cleaning up.', + ['app' => 'user_ldap'] + ); + return false; + } + } + $this->logger->info('Cleaning up after user ' . $uid, + ['app' => 'user_ldap']); + + $this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core + $this->access->userManager->invalidate($uid); + $this->access->connection->clearCache(); + return true; + } + + /** + * get the user's home directory + * + * @param string $uid the username + * @return bool|string + * @throws NoUserException + * @throws \Exception + */ + public function getHome($uid) { + // user Exists check required as it is not done in user proxy! + if (!$this->userExists($uid)) { + return false; + } + + if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) { + return $this->userPluginManager->getHome($uid); + } + + $cacheKey = 'getHome' . $uid; + $path = $this->access->connection->getFromCache($cacheKey); + if (!is_null($path)) { + return $path; + } + + // early return path if it is a deleted user + $user = $this->access->userManager->get($uid); + if ($user instanceof User || $user instanceof OfflineUser) { + $path = $user->getHomePath() ?: false; + } else { + throw new NoUserException($uid . ' is not a valid user anymore'); + } + + $this->access->cacheUserHome($uid, $path); + return $path; + } + + /** + * get display name of the user + * @param string $uid user ID of the user + * @return string|false display name + */ + public function getDisplayName($uid) { + if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) { + return $this->userPluginManager->getDisplayName($uid); + } + + if (!$this->userExists($uid)) { + return false; + } + + $cacheKey = 'getDisplayName' . $uid; + if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) { + return $displayName; + } + + //Check whether the display name is configured to have a 2nd feature + $additionalAttribute = $this->access->connection->ldapUserDisplayName2; + $displayName2 = ''; + if ($additionalAttribute !== '') { + $displayName2 = $this->access->readAttribute( + $this->access->username2dn($uid), + $additionalAttribute); + } + + $displayName = $this->access->readAttribute( + $this->access->username2dn($uid), + $this->access->connection->ldapUserDisplayName); + + if ($displayName && (count($displayName) > 0)) { + $displayName = $displayName[0]; + + if (is_array($displayName2)) { + $displayName2 = count($displayName2) > 0 ? $displayName2[0] : ''; + } + + $user = $this->access->userManager->get($uid); + if ($user instanceof User) { + $displayName = $user->composeAndStoreDisplayName($displayName, (string)$displayName2); + $this->access->connection->writeToCache($cacheKey, $displayName); + } + if ($user instanceof OfflineUser) { + $displayName = $user->getDisplayName(); + } + return $displayName; + } + + return null; + } + + /** + * set display name of the user + * @param string $uid user ID of the user + * @param string $displayName new display name of the user + * @return string|false display name + */ + public function setDisplayName($uid, $displayName) { + if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) { + $this->userPluginManager->setDisplayName($uid, $displayName); + $this->access->cacheUserDisplayName($uid, $displayName); + return $displayName; + } + return false; + } + + /** + * Get a list of all display names + * + * @param string $search + * @param int|null $limit + * @param int|null $offset + * @return array an array of all displayNames (value) and the corresponding uids (key) + */ + public function getDisplayNames($search = '', $limit = null, $offset = null) { + $cacheKey = 'getDisplayNames-' . $search . '-' . $limit . '-' . $offset; + if (!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) { + return $displayNames; + } + + $displayNames = []; + $users = $this->getUsers($search, $limit, $offset); + foreach ($users as $user) { + $displayNames[$user] = $this->getDisplayName($user); + } + $this->access->connection->writeToCache($cacheKey, $displayNames); + return $displayNames; + } + + /** + * Check if backend implements actions + * @param int $actions bitwise-or'ed actions + * @return boolean + * + * Returns the supported actions as int to be + * compared with \OC\User\Backend::CREATE_USER etc. + */ + public function implementsActions($actions) { + return (bool)((Backend::CHECK_PASSWORD + | Backend::GET_HOME + | Backend::GET_DISPLAYNAME + | (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0) + | Backend::COUNT_USERS + | (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0) + | $this->userPluginManager->getImplementedActions()) + & $actions); + } + + /** + * @return bool + */ + public function hasUserListings() { + return true; + } + + /** + * counts the users in LDAP + */ + public function countUsers(int $limit = 0): int|false { + if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) { + return $this->userPluginManager->countUsers(); + } + + $filter = $this->access->getFilterForUserCount(); + $cacheKey = 'countUsers-' . $filter . '-' . $limit; + if (!is_null($entries = $this->access->connection->getFromCache($cacheKey))) { + return $entries; + } + $entries = $this->access->countUsers($filter, limit:$limit); + $this->access->connection->writeToCache($cacheKey, $entries); + return $entries; + } + + public function countMappedUsers(): int { + return $this->access->getUserMapper()->count(); + } + + /** + * Backend name to be shown in user management + * @return string the name of the backend to be shown + */ + public function getBackendName() { + return 'LDAP'; + } + + /** + * Return access for LDAP interaction. + * @param string $uid + * @return Access instance of Access for LDAP interaction + */ + public function getLDAPAccess($uid) { + return $this->access; + } + + /** + * Return LDAP connection resource from a cloned connection. + * The cloned connection needs to be closed manually. + * of the current access. + * @param string $uid + * @return \LDAP\Connection The LDAP connection + */ + public function getNewLDAPConnection($uid) { + $connection = clone $this->access->getConnection(); + return $connection->getConnectionResource(); + } + + /** + * create new user + * @param string $username username of the new user + * @param string $password password of the new user + * @throws \UnexpectedValueException + * @return bool + */ + public function createUser($username, $password) { + if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) { + if ($dn = $this->userPluginManager->createUser($username, $password)) { + if (is_string($dn)) { + // the NC user creation work flow requires a know user id up front + $uuid = $this->access->getUUID($dn, true); + if (is_string($uuid)) { + $this->access->mapAndAnnounceIfApplicable( + $this->access->getUserMapper(), + $dn, + $username, + $uuid, + true + ); + } else { + $this->logger->warning( + 'Failed to map created LDAP user with userid {userid}, because UUID could not be determined', + [ + 'app' => 'user_ldap', + 'userid' => $username, + ] + ); + } + } else { + throw new \UnexpectedValueException('LDAP Plugin: Method createUser changed to return the user DN instead of boolean.'); + } + } + return (bool)$dn; + } + return false; + } + + public function isUserEnabled(string $uid, callable $queryDatabaseValue): bool { + if ($this->deletedUsersIndex->isUserMarked($uid) && ((int)$this->access->connection->markRemnantsAsDisabled === 1)) { + return false; + } else { + return $queryDatabaseValue(); + } + } + + public function setUserEnabled(string $uid, bool $enabled, callable $queryDatabaseValue, callable $setDatabaseValue): bool { + $setDatabaseValue($enabled); + return $enabled; + } + + public function getDisabledUserList(?int $limit = null, int $offset = 0, string $search = ''): array { + throw new \Exception('This is implemented directly in User_Proxy'); + } +} |