* @author Christoph Wurst * @author Joas Schilling * @author Jörn Friedrich Dreyer * @author Marc Hefter * @author Morris Jobke * @author Roeland Jago Douma * @author Roger Szabo * @author Thomas Müller * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ namespace OCA\User_LDAP\User; use OCA\User_LDAP\Access; use OCA\User_LDAP\FilesystemHelper; use OCP\Cache\CappedMemoryCache; use OCP\IAvatarManager; use OCP\IConfig; use OCP\IDBConnection; use OCP\Image; use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; use OCP\Share\IManager; use Psr\Log\LoggerInterface; /** * Manager * * upon request, returns an LDAP user object either by creating or from run-time * cache */ class Manager { protected ?Access $access = null; protected IConfig $ocConfig; protected IDBConnection $db; protected IUserManager $userManager; protected INotificationManager $notificationManager; protected FilesystemHelper $ocFilesystem; protected LoggerInterface $logger; protected Image $image; protected IAvatarManager $avatarManager; /** @var CappedMemoryCache $usersByDN */ protected CappedMemoryCache $usersByDN; /** @var CappedMemoryCache $usersByUid */ protected CappedMemoryCache $usersByUid; private IManager $shareManager; public function __construct( IConfig $ocConfig, FilesystemHelper $ocFilesystem, LoggerInterface $logger, IAvatarManager $avatarManager, Image $image, IUserManager $userManager, INotificationManager $notificationManager, IManager $shareManager ) { $this->ocConfig = $ocConfig; $this->ocFilesystem = $ocFilesystem; $this->logger = $logger; $this->avatarManager = $avatarManager; $this->image = $image; $this->userManager = $userManager; $this->notificationManager = $notificationManager; $this->usersByDN = new CappedMemoryCache(); $this->usersByUid = new CappedMemoryCache(); $this->shareManager = $shareManager; } /** * Binds manager to an instance of Access. * It needs to be assigned first before the manager can be used. * @param Access */ public function setLdapAccess(Access $access) { $this->access = $access; } /** * @brief creates an instance of User and caches (just runtime) it in the * property array * @param string $dn the DN of the user * @param string $uid the internal (owncloud) username * @return \OCA\User_LDAP\User\User */ private function createAndCache($dn, $uid) { $this->checkAccess(); $user = new User($uid, $dn, $this->access, $this->ocConfig, $this->ocFilesystem, clone $this->image, $this->logger, $this->avatarManager, $this->userManager, $this->notificationManager); $this->usersByDN[$dn] = $user; $this->usersByUid[$uid] = $user; return $user; } /** * removes a user entry from the cache * @param $uid */ public function invalidate($uid) { if (!isset($this->usersByUid[$uid])) { return; } $dn = $this->usersByUid[$uid]->getDN(); unset($this->usersByUid[$uid]); unset($this->usersByDN[$dn]); } /** * @brief checks whether the Access instance has been set * @throws \Exception if Access has not been set * @return null */ private function checkAccess() { if (is_null($this->access)) { throw new \Exception('LDAP Access instance must be set first'); } } /** * returns a list of attributes that will be processed further, e.g. quota, * email, displayname, or others. * * @param bool $minimal - optional, set to true to skip attributes with big * payload * @return string[] */ public function getAttributes($minimal = false) { $baseAttributes = array_merge(Access::UUID_ATTRIBUTES, ['dn', 'uid', 'samaccountname', 'memberof']); $attributes = [ $this->access->getConnection()->ldapExpertUUIDUserAttr, $this->access->getConnection()->ldapQuotaAttribute, $this->access->getConnection()->ldapEmailAttribute, $this->access->getConnection()->ldapUserDisplayName, $this->access->getConnection()->ldapUserDisplayName2, $this->access->getConnection()->ldapExtStorageHomeAttribute, $this->access->getConnection()->ldapAttributePhone, $this->access->getConnection()->ldapAttributeWebsite, $this->access->getConnection()->ldapAttributeAddress, $this->access->getConnection()->ldapAttributeTwitter, $this->access->getConnection()->ldapAttributeFediverse, $this->access->getConnection()->ldapAttributeOrganisation, $this->access->getConnection()->ldapAttributeRole, $this->access->getConnection()->ldapAttributeHeadline, $this->access->getConnection()->ldapAttributeBiography, ]; $homeRule = (string)$this->access->getConnection()->homeFolderNamingRule; if (str_starts_with($homeRule, 'attr:')) { $attributes[] = substr($homeRule, strlen('attr:')); } if (!$minimal) { // attributes that are not really important but may come with big // payload. $attributes = array_merge( $attributes, $this->access->getConnection()->resolveRule('avatar') ); } $attributes = array_reduce($attributes, function ($list, $attribute) { $attribute = strtolower(trim((string)$attribute)); if (!empty($attribute) && !in_array($attribute, $list)) { $list[] = $attribute; } return $list; }, $baseAttributes // hard-coded, lower-case, non-empty attributes ); return $attributes; } /** * Checks whether the specified user is marked as deleted * @param string $id the Nextcloud user name * @return bool */ public function isDeletedUser($id) { $isDeleted = $this->ocConfig->getUserValue( $id, 'user_ldap', 'isDeleted', 0); return (int)$isDeleted === 1; } /** * creates and returns an instance of OfflineUser for the specified user * @param string $id * @return \OCA\User_LDAP\User\OfflineUser */ public function getDeletedUser($id) { return new OfflineUser( $id, $this->ocConfig, $this->access->getUserMapper(), $this->shareManager ); } /** * @brief returns a User object by its Nextcloud username * @param string $id the DN or username of the user * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null */ protected function createInstancyByUserName($id) { //most likely a uid. Check whether it is a deleted user if ($this->isDeletedUser($id)) { return $this->getDeletedUser($id); } $dn = $this->access->username2dn($id); if ($dn !== false) { return $this->createAndCache($dn, $id); } return null; } /** * @brief returns a User object by its DN or Nextcloud username * @param string $id the DN or username of the user * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null * @throws \Exception when connection could not be established */ public function get($id) { $this->checkAccess(); if (isset($this->usersByDN[$id])) { return $this->usersByDN[$id]; } elseif (isset($this->usersByUid[$id])) { return $this->usersByUid[$id]; } if ($this->access->stringResemblesDN($id)) { $uid = $this->access->dn2username($id); if ($uid !== false) { return $this->createAndCache($id, $uid); } } return $this->createInstancyByUserName($id); } }