diff options
Diffstat (limited to 'apps/user_ldap/lib')
70 files changed, 1366 insertions, 1203 deletions
diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 7134706ebfc..9fe0aa64268 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -15,9 +15,15 @@ use OCA\User_LDAP\Exceptions\NoMoreResults; use OCA\User_LDAP\Mapping\AbstractMapping; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\OfflineUser; +use OCP\EventDispatcher\IEventDispatcher; use OCP\HintException; +use OCP\IAppConfig; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IUserManager; +use OCP\Server; +use OCP\User\Events\UserIdAssignedEvent; +use OCP\Util; use Psr\Log\LoggerInterface; use function strlen; use function substr; @@ -30,10 +36,6 @@ use function substr; class Access extends LDAPUtility { public const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid']; - /** @var \OCA\User_LDAP\Connection */ - public $connection; - /** @var Manager */ - public $userManager; /** * never ever check this var directly, always use getPagedSearchResultState * @var ?bool @@ -46,35 +48,21 @@ class Access extends LDAPUtility { /** @var ?AbstractMapping */ protected $groupMapper; - /** - * @var \OCA\User_LDAP\Helper - */ - private $helper; - /** @var IConfig */ - private $config; - /** @var IUserManager */ - private $ncUserManager; - /** @var LoggerInterface */ - private $logger; private string $lastCookie = ''; public function __construct( - Connection $connection, ILDAPWrapper $ldap, - Manager $userManager, - Helper $helper, - IConfig $config, - IUserManager $ncUserManager, - LoggerInterface $logger + public Connection $connection, + public Manager $userManager, + private Helper $helper, + private IConfig $config, + private IUserManager $ncUserManager, + private LoggerInterface $logger, + private IAppConfig $appConfig, + private IEventDispatcher $dispatcher, ) { parent::__construct($ldap); - $this->connection = $connection; - $this->userManager = $userManager; $this->userManager->setLdapAccess($this); - $this->helper = $helper; - $this->config = $config; - $this->ncUserManager = $ncUserManager; - $this->logger = $logger; } /** @@ -130,14 +118,64 @@ class Access extends LDAPUtility { } /** + * Reads several attributes for an LDAP record identified by a DN and a filter + * No support for ranged attributes. + * + * @param string $dn the record in question + * @param array $attrs the attributes that shall be retrieved + * if empty, just check the record's existence + * @param string $filter + * @return array|false an array of values on success or an empty + * array if $attr is empty, false otherwise + * @throws ServerNotAvailableException + */ + public function readAttributes(string $dn, array $attrs, string $filter = 'objectClass=*'): array|false { + if (!$this->checkConnection()) { + $this->logger->warning( + 'No LDAP Connector assigned, access impossible for readAttribute.', + ['app' => 'user_ldap'] + ); + return false; + } + $cr = $this->connection->getConnectionResource(); + $attrs = array_map( + fn (string $attr): string => mb_strtolower($attr, 'UTF-8'), + $attrs, + ); + + $values = []; + $record = $this->executeRead($dn, $attrs, $filter); + if (is_bool($record)) { + // when an exists request was run and it was successful, an empty + // array must be returned + return $record ? [] : false; + } + + $result = []; + foreach ($attrs as $attr) { + $values = $this->extractAttributeValuesFromResult($record, $attr); + if (!empty($values)) { + $result[$attr] = $values; + } + } + + if (!empty($result)) { + return $result; + } + + $this->logger->debug('Requested attributes {attrs} not found for ' . $dn, ['app' => 'user_ldap', 'attrs' => $attrs]); + return false; + } + + /** * reads a given attribute for an LDAP record identified by a DN * * @param string $dn the record in question * @param string $attr the attribute that shall be retrieved - * if empty, just check the record's existence + * if empty, just check the record's existence * @param string $filter * @return array|false an array of values on success or an empty - * array if $attr is empty, false otherwise + * array if $attr is empty, false otherwise * @throws ServerNotAvailableException */ public function readAttribute(string $dn, string $attr, string $filter = 'objectClass=*') { @@ -204,9 +242,9 @@ class Access extends LDAPUtility { * returned data on a successful usual operation * @throws ServerNotAvailableException */ - public function executeRead(string $dn, string $attribute, string $filter) { + public function executeRead(string $dn, string|array $attribute, string $filter) { $dn = $this->helper->DNasBaseParameter($dn); - $rr = @$this->invokeLDAPMethod('read', $dn, $filter, [$attribute]); + $rr = @$this->invokeLDAPMethod('read', $dn, $filter, (is_string($attribute) ? [$attribute] : $attribute)); if (!$this->ldap->isResource($rr)) { if ($attribute !== '') { //do not throw this message on userExists check, irritates @@ -225,7 +263,7 @@ class Access extends LDAPUtility { return false; } //LDAP attributes are not case sensitive - $result = \OCP\Util::mb_array_change_key_case( + $result = Util::mb_array_change_key_case( $this->invokeLDAPMethod('getAttributes', $er), MB_CASE_LOWER, 'UTF-8'); return $result; @@ -268,20 +306,19 @@ class Access extends LDAPUtility { * @return array If a range was detected with keys 'values', 'attributeName', * 'attributeFull' and 'rangeHigh', otherwise empty. */ - public function extractRangeData($result, $attribute) { + public function extractRangeData(array $result, string $attribute): array { $keys = array_keys($result); foreach ($keys as $key) { if ($key !== $attribute && str_starts_with((string)$key, $attribute)) { $queryData = explode(';', (string)$key); - if (str_starts_with($queryData[1], 'range=')) { + if (isset($queryData[1]) && str_starts_with($queryData[1], 'range=')) { $high = substr($queryData[1], 1 + strpos($queryData[1], '-')); - $data = [ + return [ 'values' => $result[$key], 'attributeName' => $queryData[0], 'attributeFull' => $key, 'rangeHigh' => $high, ]; - return $data; } } } @@ -304,10 +341,10 @@ class Access extends LDAPUtility { $cr = $this->connection->getConnectionResource(); try { // try PASSWD extended operation first - return @$this->invokeLDAPMethod('exopPasswd', $userDN, '', $password) || - @$this->invokeLDAPMethod('modReplace', $userDN, $password); + return @$this->invokeLDAPMethod('exopPasswd', $userDN, '', $password) + || @$this->invokeLDAPMethod('modReplace', $userDN, $password); } catch (ConstraintViolationException $e) { - throw new HintException('Password change rejected.', \OCP\Util::getL10N('user_ldap')->t('Password change rejected. Hint: ') . $e->getMessage(), (int)$e->getCode()); + throw new HintException('Password change rejected.', Util::getL10N('user_ldap')->t('Password change rejected. Hint: %s', $e->getMessage()), (int)$e->getCode()); } } @@ -401,10 +438,11 @@ class Access extends LDAPUtility { * * @param string $fdn the dn of the group object * @param string $ldapName optional, the display name of the object + * @param bool $autoMapping Should the group be mapped if not yet mapped * @return string|false with the name to use in Nextcloud, false on DN outside of search DN * @throws \Exception */ - public function dn2groupname($fdn, $ldapName = null) { + public function dn2groupname($fdn, $ldapName = null, bool $autoMapping = true) { //To avoid bypassing the base DN settings under certain circumstances //with the group support, check whether the provided DN matches one of //the given Bases @@ -412,7 +450,7 @@ class Access extends LDAPUtility { return false; } - return $this->dn2ocname($fdn, $ldapName, false); + return $this->dn2ocname($fdn, $ldapName, false, autoMapping:$autoMapping); } /** @@ -423,7 +461,7 @@ class Access extends LDAPUtility { * @return string|false with with the name to use in Nextcloud * @throws \Exception */ - public function dn2username($fdn, $ldapName = null) { + public function dn2username($fdn) { //To avoid bypassing the base DN settings under certain circumstances //with the group support, check whether the provided DN matches one of //the given Bases @@ -431,7 +469,7 @@ class Access extends LDAPUtility { return false; } - return $this->dn2ocname($fdn, $ldapName, true); + return $this->dn2ocname($fdn, null, true); } /** @@ -442,10 +480,11 @@ class Access extends LDAPUtility { * @param bool $isUser optional, whether it is a user object (otherwise group assumed) * @param bool|null $newlyMapped * @param array|null $record + * @param bool $autoMapping Should the group be mapped if not yet mapped * @return false|string with with the name to use in Nextcloud * @throws \Exception */ - public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, ?array $record = null) { + public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, ?array $record = null, bool $autoMapping = true) { static $intermediates = []; if (isset($intermediates[($isUser ? 'user-' : 'group-') . $fdn])) { return false; // is a known intermediate @@ -454,12 +493,8 @@ class Access extends LDAPUtility { $newlyMapped = false; if ($isUser) { $mapper = $this->getUserMapper(); - $nameAttribute = $this->connection->ldapUserDisplayName; - $filter = $this->connection->ldapUserFilter; } else { $mapper = $this->getGroupMapper(); - $nameAttribute = $this->connection->ldapGroupDisplayName; - $filter = $this->connection->ldapGroupFilter; } //let's try to retrieve the Nextcloud name from the mappings table @@ -468,6 +503,41 @@ class Access extends LDAPUtility { return $ncName; } + if (!$autoMapping) { + /* If no auto mapping, stop there */ + return false; + } + + if ($isUser) { + $nameAttribute = strtolower($this->connection->ldapUserDisplayName); + $filter = $this->connection->ldapUserFilter; + $uuidAttr = 'ldapUuidUserAttribute'; + $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; + $usernameAttribute = strtolower($this->connection->ldapExpertUsernameAttr); + $attributesToRead = [$nameAttribute,$usernameAttribute]; + // TODO fetch also display name attributes and cache them if the user is mapped + } else { + $nameAttribute = strtolower($this->connection->ldapGroupDisplayName); + $filter = $this->connection->ldapGroupFilter; + $uuidAttr = 'ldapUuidGroupAttribute'; + $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr; + $attributesToRead = [$nameAttribute]; + } + + if ($this->detectUuidAttribute($fdn, $isUser, false, $record)) { + $attributesToRead[] = $this->connection->$uuidAttr; + } + + if ($record === null) { + /* No record was passed, fetch it */ + $record = $this->readAttributes($fdn, $attributesToRead, $filter); + if ($record === false) { + $this->logger->debug('Cannot read attributes for ' . $fdn . '. Skipping.', ['filter' => $filter]); + $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true; + return false; + } + } + //second try: get the UUID and check if it is known. Then, update the DN and return the name. $uuid = $this->getUUID($fdn, $isUser, $record); if (is_string($uuid)) { @@ -482,20 +552,9 @@ class Access extends LDAPUtility { return false; } - if (is_null($ldapName)) { - $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter); - if (!isset($ldapName[0]) || empty($ldapName[0])) { - $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']); - $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true; - return false; - } - $ldapName = $ldapName[0]; - } - if ($isUser) { - $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr; if ($usernameAttribute !== '') { - $username = $this->readAttribute($fdn, $usernameAttribute); + $username = $record[$usernameAttribute]; if (!isset($username[0]) || empty($username[0])) { $this->logger->debug('No or empty username (' . $usernameAttribute . ') for ' . $fdn . '.', ['app' => 'user_ldap']); return false; @@ -517,6 +576,15 @@ class Access extends LDAPUtility { return false; } } else { + if (is_null($ldapName)) { + $ldapName = $record[$nameAttribute]; + if (!isset($ldapName[0]) || empty($ldapName[0])) { + $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']); + $intermediates['group-' . $fdn] = true; + return false; + } + $ldapName = $ldapName[0]; + } $intName = $this->sanitizeGroupIDCandidate($ldapName); } @@ -528,12 +596,13 @@ class Access extends LDAPUtility { $this->connection->setConfiguration(['ldapCacheTTL' => 0]); if ($intName !== '' && (($isUser && !$this->ncUserManager->userExists($intName)) - || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName)) + || (!$isUser && !Server::get(IGroupManager::class)->groupExists($intName)) ) ) { $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]); $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser); if ($newlyMapped) { + $this->logger->debug('Mapped {fdn} as {name}', ['fdn' => $fdn,'name' => $intName]); return $intName; } } @@ -548,7 +617,6 @@ class Access extends LDAPUtility { 'fdn' => $fdn, 'altName' => $altName, 'intName' => $intName, - 'app' => 'user_ldap', ] ); $newlyMapped = true; @@ -566,13 +634,16 @@ class Access extends LDAPUtility { string $fdn, string $name, string $uuid, - bool $isUser + bool $isUser, ): bool { if ($mapper->map($fdn, $name, $uuid)) { - if ($this->ncUserManager instanceof PublicEmitter && $isUser) { + if ($isUser) { $this->cacheUserExists($name); - $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]); - } elseif (!$isUser) { + $this->dispatcher->dispatchTyped(new UserIdAssignedEvent($name)); + if ($this->ncUserManager instanceof PublicEmitter) { + $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]); + } + } else { $this->cacheGroupExists($name); } return true; @@ -673,6 +744,7 @@ class Access extends LDAPUtility { */ public function cacheUserExists(string $ocName): void { $this->connection->writeToCache('userExists' . $ocName, true); + $this->connection->writeToCache('userExistsOnLDAP' . $ocName, true); } /** @@ -742,7 +814,7 @@ class Access extends LDAPUtility { * "Developers" */ private function _createAltInternalOwnCloudNameForGroups(string $name) { - $usedNames = $this->getGroupMapper()->getNamesBySearch($name, "", '_%'); + $usedNames = $this->getGroupMapper()->getNamesBySearch($name, '', '_%'); if (count($usedNames) === 0) { $lastNo = 1; //will become name_2 } else { @@ -758,7 +830,7 @@ class Access extends LDAPUtility { // Check to be really sure it is unique // while loop is just a precaution. If a name is not generated within // 20 attempts, something else is very wrong. Avoids infinite loop. - if (!\OC::$server->getGroupManager()->groupExists($altName)) { + if (!Server::get(IGroupManager::class)->groupExists($altName)) { return $altName; } $altName = $name . '_' . ($lastNo + $attempts); @@ -822,8 +894,7 @@ class Access extends LDAPUtility { $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset); $recordsToUpdate = $ldapRecords; if (!$forceApplyAttributes) { - $isBackgroundJobModeAjax = $this->config - ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; + $isBackgroundJobModeAjax = $this->appConfig->getValueString('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; $listOfDNs = array_reduce($ldapRecords, function ($listOfDNs, $entry) { $listOfDNs[] = $entry['dn'][0]; return $listOfDNs; @@ -887,22 +958,6 @@ class Access extends LDAPUtility { } $groupRecords = $this->searchGroups($filter, $attr, $limit, $offset); - $listOfDNs = array_reduce($groupRecords, function ($listOfDNs, $entry) { - $listOfDNs[] = $entry['dn'][0]; - return $listOfDNs; - }, []); - $idsByDn = $this->getGroupMapper()->getListOfIdsByDn($listOfDNs); - - array_walk($groupRecords, function (array $record) use ($idsByDn) { - $newlyMapped = false; - $gid = $idsByDn[$record['dn'][0]] ?? null; - if ($gid === null) { - $gid = $this->dn2ocname($record['dn'][0], null, false, $newlyMapped, $record); - } - if (!$newlyMapped && is_string($gid)) { - $this->cacheGroupExists($gid); - } - }); $listOfGroups = $this->fetchList($groupRecords, $this->manyAttributes($attr)); $this->connection->writeToCache($cacheKey, $listOfGroups); return $listOfGroups; @@ -1049,7 +1104,7 @@ class Access extends LDAPUtility { * @param int|null $limit optional, maximum results to be counted * @param int|null $offset optional, a starting point * @return array|false array with the search result as first value and pagedSearchOK as - * second | false if not successful + * second | false if not successful * @throws ServerNotAvailableException */ private function executeSearch( @@ -1057,7 +1112,7 @@ class Access extends LDAPUtility { string $base, ?array &$attr, ?int $pageSize, - ?int $offset + ?int $offset, ) { // See if we have a resource, in case not cancel with message $cr = $this->connection->getConnectionResource(); @@ -1088,7 +1143,7 @@ class Access extends LDAPUtility { * @param int $limit maximum results to be counted * @param bool $pagedSearchOK whether a paged search has been executed * @param bool $skipHandling required for paged search when cookies to - * prior results need to be gained + * prior results need to be gained * @return bool cookie validity, true if we have more pages, false otherwise. * @throws ServerNotAvailableException */ @@ -1097,7 +1152,7 @@ class Access extends LDAPUtility { int $foundItems, int $limit, bool $pagedSearchOK, - bool $skipHandling + bool $skipHandling, ): bool { $cookie = ''; if ($pagedSearchOK) { @@ -1111,7 +1166,7 @@ class Access extends LDAPUtility { return false; } // if count is bigger, then the server does not support - // paged search. Instead, he did a normal search. We set a + // paged search. Instead, they did a normal search. We set a // flag here, so the callee knows how to deal with it. if ($foundItems <= $limit) { $this->pagedSearchedSuccessful = true; @@ -1138,11 +1193,11 @@ class Access extends LDAPUtility { * @param string $filter the LDAP filter for the search * @param array $bases an array containing the LDAP subtree(s) that shall be searched * @param ?string[] $attr optional, array, one or more attributes that shall be - * retrieved. Results will according to the order in the array. + * retrieved. Results will according to the order in the array. * @param int $limit maximum results to be counted, 0 means no limit * @param int $offset a starting point, defaults to 0 * @param bool $skipHandling indicates whether the pages search operation is - * completed + * completed * @return int|false Integer or false if the search could not be initialized * @throws ServerNotAvailableException */ @@ -1152,7 +1207,7 @@ class Access extends LDAPUtility { ?array $attr = null, int $limit = 0, int $offset = 0, - bool $skipHandling = false + bool $skipHandling = false, ) { $this->logger->debug('Count filter: {filter}', [ 'app' => 'user_ldap', @@ -1217,7 +1272,7 @@ class Access extends LDAPUtility { ?array $attr = null, ?int $limit = null, ?int $offset = null, - bool $skipHandling = false + bool $skipHandling = false, ): array { $limitPerPage = (int)$this->connection->ldapPagingSize; if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) { @@ -1268,7 +1323,7 @@ class Access extends LDAPUtility { if (!is_array($item)) { continue; } - $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8'); + $item = Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8'); foreach ($attr as $key) { if (isset($item[$key])) { if (is_array($item[$key]) && isset($item[$key]['count'])) { @@ -1388,7 +1443,7 @@ class Access extends LDAPUtility { * * @param string[] $filters the filters to connect * @return string the combined filter - * Combines Filter arguments with OR + * Combines Filter arguments with OR */ public function combineFilterWithOr($filters) { return $this->combineFilter($filters, '|'); @@ -1443,7 +1498,7 @@ class Access extends LDAPUtility { * * @param string $search the search term * @param string[]|null|'' $searchAttributes needs to have at least two attributes, - * otherwise it does not make sense :) + * otherwise it does not make sense :) * @return string the final filter part to use in LDAP searches * @throws DomainException */ @@ -1471,7 +1526,7 @@ class Access extends LDAPUtility { * @param string $search the search term * @param string[]|null|'' $searchAttributes * @param string $fallbackAttribute a fallback attribute in case the user - * did not define search attributes. Typically the display name attribute. + * did not define search attributes. Typically the display name attribute. * @return string the final filter part to use in LDAP searches */ private function getFilterPartForSearch(string $search, $searchAttributes, string $fallbackAttribute): string { @@ -1517,7 +1572,7 @@ class Access extends LDAPUtility { * a * */ private function prepareSearchTerm(string $term): string { - $config = \OC::$server->getConfig(); + $config = Server::get(IConfig::class); $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'); @@ -1756,8 +1811,8 @@ class Access extends LDAPUtility { * user. Instead we write a log message. */ $this->logger->info( - 'Passed string does not resemble a valid GUID. Known UUID ' . - '({uuid}) probably does not match UUID configuration.', + 'Passed string does not resemble a valid GUID. Known UUID ' + . '({uuid}) probably does not match UUID configuration.', ['app' => 'user_ldap', 'uuid' => $guid] ); return $guid; @@ -1922,7 +1977,7 @@ class Access extends LDAPUtility { string $base, ?array $attr, int $pageSize, - int $offset + int $offset, ): array { $pagedSearchOK = false; if ($pageSize !== 0) { diff --git a/apps/user_ldap/lib/AccessFactory.php b/apps/user_ldap/lib/AccessFactory.php index 2e5ee28d6d5..da114c467a7 100644 --- a/apps/user_ldap/lib/AccessFactory.php +++ b/apps/user_ldap/lib/AccessFactory.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,41 +7,38 @@ namespace OCA\User_LDAP; use OCA\User_LDAP\User\Manager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IUserManager; use OCP\Server; use Psr\Log\LoggerInterface; class AccessFactory { - private ILDAPWrapper $ldap; - private Helper $helper; - private IConfig $config; - private IUserManager $ncUserManager; - private LoggerInterface $logger; public function __construct( - ILDAPWrapper $ldap, - Helper $helper, - IConfig $config, - IUserManager $ncUserManager, - LoggerInterface $logger) { - $this->ldap = $ldap; - $this->helper = $helper; - $this->config = $config; - $this->ncUserManager = $ncUserManager; - $this->logger = $logger; + private ILDAPWrapper $ldap, + private Helper $helper, + private IConfig $config, + private IAppConfig $appConfig, + private IUserManager $ncUserManager, + private LoggerInterface $logger, + private IEventDispatcher $dispatcher, + ) { } public function get(Connection $connection): Access { /* Each Access instance gets its own Manager instance, see OCA\User_LDAP\AppInfo\Application::register() */ return new Access( - $connection, $this->ldap, + $connection, Server::get(Manager::class), $this->helper, $this->config, $this->ncUserManager, - $this->logger + $this->logger, + $this->appConfig, + $this->dispatcher, ); } } diff --git a/apps/user_ldap/lib/AppInfo/Application.php b/apps/user_ldap/lib/AppInfo/Application.php index 400315442bb..70b7920f7ab 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,7 +11,6 @@ use OCA\Files_External\Service\BackendService; use OCA\User_LDAP\Controller\RenewPasswordController; use OCA\User_LDAP\Events\GroupBackendRegistered; use OCA\User_LDAP\Events\UserBackendRegistered; -use OCA\User_LDAP\FilesystemHelper; use OCA\User_LDAP\Group_Proxy; use OCA\User_LDAP\GroupPluginManager; use OCA\User_LDAP\Handler\ExtStorageConfigHandler; @@ -40,6 +40,7 @@ use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; use OCP\Share\IManager as IShareManager; use OCP\User\Events\PostLoginEvent; +use OCP\Util; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -84,7 +85,6 @@ class Application extends App implements IBootstrap { function (ContainerInterface $c) { return new Manager( $c->get(IConfig::class), - $c->get(FilesystemHelper::class), $c->get(LoggerInterface::class), $c->get(IAvatarManager::class), $c->get(Image::class), @@ -106,17 +106,18 @@ class Application extends App implements IBootstrap { INotificationManager $notificationManager, IAppContainer $appContainer, IEventDispatcher $dispatcher, + IUserManager $userManager, IGroupManager $groupManager, User_Proxy $userBackend, Group_Proxy $groupBackend, - Helper $helper - ) { + Helper $helper, + ): void { $configPrefixes = $helper->getServerConfigurationPrefixes(true); if (count($configPrefixes) > 0) { $userPluginManager = $appContainer->get(UserPluginManager::class); $groupPluginManager = $appContainer->get(GroupPluginManager::class); - \OC_User::useBackend($userBackend); + $userManager->registerBackend($userBackend); $groupManager->addBackend($groupBackend); $userBackendRegisteredEvent = new UserBackendRegistered($userBackend, $userPluginManager); @@ -129,7 +130,7 @@ class Application extends App implements IBootstrap { $context->injectFn(Closure::fromCallable([$this, 'registerBackendDependents'])); - \OCP\Util::connectHook( + Util::connectHook( '\OCA\Files_Sharing\API\Server2Server', 'preLoginNameUsedAsUserName', '\OCA\User_LDAP\Helper', @@ -140,7 +141,7 @@ class Application extends App implements IBootstrap { private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher): void { $dispatcher->addListener( 'OCA\\Files_External::loadAdditionalBackends', - function () use ($appContainer) { + function () use ($appContainer): void { $storagesBackendService = $appContainer->get(BackendService::class); $storagesBackendService->registerConfigHandler('home', function () use ($appContainer) { return $appContainer->get(ExtStorageConfigHandler::class); diff --git a/apps/user_ldap/lib/BackendUtility.php b/apps/user_ldap/lib/BackendUtility.php index 0e797abb8dc..88d7311cde0 100644 --- a/apps/user_ldap/lib/BackendUtility.php +++ b/apps/user_ldap/lib/BackendUtility.php @@ -8,13 +8,12 @@ namespace OCA\User_LDAP; abstract class BackendUtility { - protected $access; - /** * constructor, make sure the subclasses call this one! * @param Access $access an instance of Access for LDAP interaction */ - public function __construct(Access $access) { - $this->access = $access; + public function __construct( + protected Access $access, + ) { } } diff --git a/apps/user_ldap/lib/Command/CheckGroup.php b/apps/user_ldap/lib/Command/CheckGroup.php index c376d0f890a..9c7ccb9d3b3 100644 --- a/apps/user_ldap/lib/Command/CheckGroup.php +++ b/apps/user_ldap/lib/Command/CheckGroup.php @@ -99,25 +99,25 @@ class CheckGroup extends Command { throw new \Exception('The given group is not a recognized LDAP group.'); } catch (\Exception $e) { - $output->writeln('<error>' . $e->getMessage(). '</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); return self::FAILURE; } } public function onGroupCreatedEvent(GroupCreatedEvent $event, OutputInterface $output): void { - $output->writeln('<info>The group '.$event->getGroup()->getGID().' was added to Nextcloud with '.$event->getGroup()->count().' users</info>'); + $output->writeln('<info>The group ' . $event->getGroup()->getGID() . ' was added to Nextcloud with ' . $event->getGroup()->count() . ' users</info>'); } public function onUserAddedEvent(UserAddedEvent $event, OutputInterface $output): void { $user = $event->getUser(); $group = $event->getGroup(); - $output->writeln('<info>The user '.$user->getUID().' was added to group '.$group->getGID().'</info>'); + $output->writeln('<info>The user ' . $user->getUID() . ' was added to group ' . $group->getGID() . '</info>'); } public function onUserRemovedEvent(UserRemovedEvent $event, OutputInterface $output): void { $user = $event->getUser(); $group = $event->getGroup(); - $output->writeln('<info>The user '.$user->getUID().' was removed from group '.$group->getGID().'</info>'); + $output->writeln('<info>The user ' . $user->getUID() . ' was removed from group ' . $group->getGID() . '</info>'); } /** diff --git a/apps/user_ldap/lib/Command/CheckUser.php b/apps/user_ldap/lib/Command/CheckUser.php index ebb416c24a6..8bb26ce3d0e 100644 --- a/apps/user_ldap/lib/Command/CheckUser.php +++ b/apps/user_ldap/lib/Command/CheckUser.php @@ -18,15 +18,12 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CheckUser extends Command { - protected User_Proxy $backend; - public function __construct( - User_Proxy $uBackend, + protected User_Proxy $backend, protected Helper $helper, protected DeletedUsersIndex $dui, protected UserMapping $mapping, ) { - $this->backend = $uBackend; parent::__construct(); } @@ -84,7 +81,7 @@ class CheckUser extends Command { throw new \Exception('The given user is not a recognized LDAP user.'); } catch (\Exception $e) { - $output->writeln('<error>' . $e->getMessage(). '</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); return self::FAILURE; } } diff --git a/apps/user_ldap/lib/Command/PromoteGroup.php b/apps/user_ldap/lib/Command/PromoteGroup.php index bb5362e4700..b203a910b14 100644 --- a/apps/user_ldap/lib/Command/PromoteGroup.php +++ b/apps/user_ldap/lib/Command/PromoteGroup.php @@ -22,7 +22,7 @@ class PromoteGroup extends Command { public function __construct( private IGroupManager $groupManager, - private Group_Proxy $backend + private Group_Proxy $backend, ) { parent::__construct(); } diff --git a/apps/user_ldap/lib/Command/ResetGroup.php b/apps/user_ldap/lib/Command/ResetGroup.php index 89d3f31f69d..5833ca980f2 100644 --- a/apps/user_ldap/lib/Command/ResetGroup.php +++ b/apps/user_ldap/lib/Command/ResetGroup.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Command/ResetUser.php b/apps/user_ldap/lib/Command/ResetUser.php index 58dfbf68519..1409806e4ac 100644 --- a/apps/user_ldap/lib/Command/ResetUser.php +++ b/apps/user_ldap/lib/Command/ResetUser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Command/Search.php b/apps/user_ldap/lib/Command/Search.php index 4886e0c7638..85906b20e9a 100644 --- a/apps/user_ldap/lib/Command/Search.php +++ b/apps/user_ldap/lib/Command/Search.php @@ -12,6 +12,7 @@ use OCA\User_LDAP\Helper; use OCA\User_LDAP\LDAP; use OCA\User_LDAP\User_Proxy; use OCP\IConfig; +use OCP\Server; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -81,7 +82,7 @@ class Search extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $helper = new Helper($this->ocConfig, \OC::$server->getDatabaseConnection()); + $helper = Server::get(Helper::class); $configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldapWrapper = new LDAP(); @@ -106,7 +107,7 @@ class Search extends Command { $result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset); foreach ($result as $id => $name) { - $line = $name . ($printID ? ' ('.$id.')' : ''); + $line = $name . ($printID ? ' (' . $id . ')' : ''); $output->writeln($line); } return self::SUCCESS; diff --git a/apps/user_ldap/lib/Command/SetConfig.php b/apps/user_ldap/lib/Command/SetConfig.php index 66a79d8c439..7e9efcf34d0 100644 --- a/apps/user_ldap/lib/Command/SetConfig.php +++ b/apps/user_ldap/lib/Command/SetConfig.php @@ -11,6 +11,7 @@ use OCA\User_LDAP\Configuration; use OCA\User_LDAP\ConnectionFactory; use OCA\User_LDAP\Helper; use OCA\User_LDAP\LDAP; +use OCP\Server; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -40,11 +41,11 @@ class SetConfig extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); + $helper = Server::get(Helper::class); $availableConfigs = $helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); if (!in_array($configID, $availableConfigs)) { - $output->writeln("Invalid configID"); + $output->writeln('Invalid configID'); return self::FAILURE; } diff --git a/apps/user_ldap/lib/Command/ShowConfig.php b/apps/user_ldap/lib/Command/ShowConfig.php index 933d1129d42..fa021192ac4 100644 --- a/apps/user_ldap/lib/Command/ShowConfig.php +++ b/apps/user_ldap/lib/Command/ShowConfig.php @@ -54,7 +54,7 @@ class ShowConfig extends Base { if (!is_null($configID)) { $configIDs[] = $configID; if (!in_array($configIDs[0], $availableConfigs)) { - $output->writeln("Invalid configID"); + $output->writeln('Invalid configID'); return self::FAILURE; } } else { diff --git a/apps/user_ldap/lib/Command/TestUserSettings.php b/apps/user_ldap/lib/Command/TestUserSettings.php new file mode 100644 index 00000000000..12690158f98 --- /dev/null +++ b/apps/user_ldap/lib/Command/TestUserSettings.php @@ -0,0 +1,248 @@ +<?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\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Mapping\UserMapping; +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCA\User_LDAP\User_Proxy; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class TestUserSettings extends Command { + public function __construct( + protected User_Proxy $backend, + protected Group_Proxy $groupBackend, + protected Helper $helper, + protected DeletedUsersIndex $dui, + protected UserMapping $mapping, + protected GroupMapping $groupMapping, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:test-user-settings') + ->setDescription('Runs tests and show information about user related LDAP settings') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'the user name as used in Nextcloud, or the LDAP DN' + ) + ->addOption( + 'group', + 'g', + InputOption::VALUE_REQUIRED, + 'A group DN to check if the user is a member or not' + ) + ->addOption( + 'clearcache', + null, + InputOption::VALUE_NONE, + 'Clear the cache of the LDAP connection before the beginning of tests' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $uid = $input->getArgument('user'); + $access = $this->backend->getLDAPAccess($uid); + $connection = $access->getConnection(); + if ($input->getOption('clearcache')) { + $connection->clearCache(); + } + $configPrefix = $connection->getConfigPrefix(); + $knownDn = ''; + if ($access->stringResemblesDN($uid)) { + $knownDn = $uid; + $username = $access->dn2username($uid); + if ($username !== false) { + $uid = $username; + } + } + + $dn = $this->mapping->getDNByName($uid); + if ($dn !== false) { + $output->writeln("User <info>$dn</info> is mapped with account name <info>$uid</info>."); + $uuid = $this->mapping->getUUIDByDN($dn); + $output->writeln("Known UUID is <info>$uuid</info>."); + if ($knownDn === '') { + $knownDn = $dn; + } + } else { + $output->writeln("User <info>$uid</info> is not mapped."); + } + + if ($knownDn === '') { + return self::SUCCESS; + } + + if (!$access->isDNPartOfBase($knownDn, $access->getConnection()->ldapBaseUsers)) { + $output->writeln( + "User <info>$knownDn</info> is not in one of the configured user bases: <info>" + . implode(',', $access->getConnection()->ldapBaseUsers) + . '</info>.' + ); + } + + $output->writeln("Configuration prefix is <info>$configPrefix</info>"); + $output->writeln(''); + + $attributeNames = [ + 'ldapBase', + 'ldapBaseUsers', + 'ldapExpertUsernameAttr', + 'ldapUuidUserAttribute', + 'ldapExpertUUIDUserAttr', + 'ldapQuotaAttribute', + 'ldapEmailAttribute', + 'ldapUserDisplayName', + 'ldapUserDisplayName2', + 'ldapExtStorageHomeAttribute', + 'ldapAttributePhone', + 'ldapAttributeWebsite', + 'ldapAttributeAddress', + 'ldapAttributeTwitter', + 'ldapAttributeFediverse', + 'ldapAttributeOrganisation', + 'ldapAttributeRole', + 'ldapAttributeHeadline', + 'ldapAttributeBiography', + 'ldapAttributeBirthDate', + 'ldapAttributePronouns', + 'ldapGidNumber', + 'hasGidNumber', + ]; + $output->writeln('Attributes set in configuration:'); + foreach ($attributeNames as $attributeName) { + if (($connection->$attributeName !== '') && ($connection->$attributeName !== [])) { + if (\is_string($connection->$attributeName)) { + $output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>'); + } else { + $output->writeln("- $attributeName: <info>" . \json_encode($connection->$attributeName) . '</info>'); + } + } + } + + $filter = $connection->ldapUserFilter; + $attrs = $access->userManager->getAttributes(true); + $attrs[] = strtolower($connection->ldapExpertUsernameAttr); + if ($connection->ldapUuidUserAttribute !== 'auto') { + $attrs[] = strtolower($connection->ldapUuidUserAttribute); + } + if ($connection->hasGidNumber) { + $attrs[] = strtolower($connection->ldapGidNumber); + } + $attrs[] = 'memberof'; + $attrs = array_values(array_unique($attrs)); + $attributes = $access->readAttributes($knownDn, $attrs, $filter); + + if ($attributes === false) { + $output->writeln( + "LDAP read on <info>$knownDn</info> with filter <info>$filter</info> failed." + ); + return self::FAILURE; + } + + $output->writeln("Attributes fetched from LDAP using filter <info>$filter</info>:"); + foreach ($attributes as $attribute => $value) { + $output->writeln( + "- $attribute: <info>" . json_encode($value) . '</info>' + ); + } + + $uuid = $access->getUUID($knownDn); + if ($connection->ldapUuidUserAttribute === 'auto') { + $output->writeln('<error>Failed to detect UUID attribute</error>'); + } else { + $output->writeln('Detected UUID attribute: <info>' . $connection->ldapUuidUserAttribute . '</info>'); + } + if ($uuid === false) { + $output->writeln("<error>Failed to find UUID for $knownDn</error>"); + } else { + $output->writeln("UUID for <info>$knownDn</info>: <info>$uuid</info>"); + } + + $groupLdapInstance = $this->groupBackend->getBackend($configPrefix); + + $output->writeln(''); + $output->writeln('Group information:'); + + $attributeNames = [ + 'ldapBaseGroups', + 'ldapDynamicGroupMemberURL', + 'ldapGroupFilter', + 'ldapGroupMemberAssocAttr', + ]; + $output->writeln('Configuration:'); + foreach ($attributeNames as $attributeName) { + if ($connection->$attributeName !== '') { + $output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>'); + } + } + + $primaryGroup = $groupLdapInstance->getUserPrimaryGroup($knownDn); + $output->writeln('Primary group: <info>' . ($primaryGroup !== false? $primaryGroup:'') . '</info>'); + + $groupByGid = $groupLdapInstance->getUserGroupByGid($knownDn); + $output->writeln('Group from gidNumber: <info>' . ($groupByGid !== false? $groupByGid:'') . '</info>'); + + $groups = $groupLdapInstance->getUserGroups($uid); + $output->writeln('All known groups: <info>' . json_encode($groups) . '</info>'); + + $memberOfUsed = ((int)$access->connection->hasMemberOfFilterSupport === 1 + && (int)$access->connection->useMemberOfToDetectMembership === 1); + + $output->writeln('MemberOf usage: <info>' . ($memberOfUsed ? 'on' : 'off') . '</info> (' . $access->connection->hasMemberOfFilterSupport . ',' . $access->connection->useMemberOfToDetectMembership . ')'); + + $gid = (string)$input->getOption('group'); + if ($gid === '') { + return self::SUCCESS; + } + + $output->writeln(''); + $output->writeln("Group $gid:"); + $knownGroupDn = ''; + if ($access->stringResemblesDN($gid)) { + $knownGroupDn = $gid; + $groupname = $access->dn2groupname($gid); + if ($groupname !== false) { + $gid = $groupname; + } + } + + $groupDn = $this->groupMapping->getDNByName($gid); + if ($groupDn !== false) { + $output->writeln("Group <info>$groupDn</info> is mapped with name <info>$gid</info>."); + $groupUuid = $this->groupMapping->getUUIDByDN($groupDn); + $output->writeln("Known UUID is <info>$groupUuid</info>."); + if ($knownGroupDn === '') { + $knownGroupDn = $groupDn; + } + } else { + $output->writeln("Group <info>$gid</info> is not mapped."); + } + + $members = $groupLdapInstance->usersInGroup($gid); + $output->writeln('Members: <info>' . json_encode($members) . '</info>'); + + return self::SUCCESS; + + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + } +} diff --git a/apps/user_ldap/lib/Command/UpdateUUID.php b/apps/user_ldap/lib/Command/UpdateUUID.php index acf4acbe7d5..93dcc37bada 100644 --- a/apps/user_ldap/lib/Command/UpdateUUID.php +++ b/apps/user_ldap/lib/Command/UpdateUUID.php @@ -266,7 +266,7 @@ class UpdateUUID extends Command { protected function handleMappingBasedUpdates(bool $invalidatedOnly): \Generator { $limit = 1000; - /** @var AbstractMapping $mapping*/ + /** @var AbstractMapping $mapping */ foreach ([$this->userMapping, $this->groupMapping] as $mapping) { $offset = 0; do { diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index 612a46d618e..b4a5b847204 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -7,6 +7,8 @@ */ namespace OCA\User_LDAP; +use OCP\IConfig; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -80,6 +82,8 @@ use Psr\Log\LoggerInterface; * @property string $ldapAttributeHeadline * @property string $ldapAttributeBiography * @property string $ldapAdminGroup + * @property string $ldapAttributeBirthDate + * @property string $ldapAttributePronouns */ class Configuration { public const AVATAR_PREFIX_DEFAULT = 'default'; @@ -89,11 +93,6 @@ class Configuration { public const LDAP_SERVER_FEATURE_UNKNOWN = 'unknown'; public const LDAP_SERVER_FEATURE_AVAILABLE = 'available'; public const LDAP_SERVER_FEATURE_UNAVAILABLE = 'unavailable'; - - /** - * @var string - */ - protected $configPrefix; /** * @var bool */ @@ -179,10 +178,13 @@ class Configuration { 'ldapAdminGroup' => '', 'ldapAttributeBirthDate' => null, 'ldapAttributeAnniversaryDate' => null, + 'ldapAttributePronouns' => null, ]; - public function __construct(string $configPrefix, bool $autoRead = true) { - $this->configPrefix = $configPrefix; + public function __construct( + protected string $configPrefix, + bool $autoRead = true, + ) { if ($autoRead) { $this->readConfiguration(); } @@ -216,7 +218,7 @@ class Configuration { * from configuration. It does not save the configuration! To do so, you * must call saveConfiguration afterwards. * @param array $config array that holds the config parameters in an associated - * array + * array * @param array &$applied optional; array where the set fields will be given to */ public function setConfiguration(array $config, ?array &$applied = null): void { @@ -238,7 +240,7 @@ class Configuration { case 'homeFolderNamingRule': $trimmedVal = trim($val); if ($trimmedVal !== '' && !str_contains($val, 'attr:')) { - $val = 'attr:'.$trimmedVal; + $val = 'attr:' . $trimmedVal; } break; case 'ldapBase': @@ -294,6 +296,28 @@ class Configuration { break; case 'ldapUserDisplayName2': case 'ldapGroupDisplayName': + case 'ldapGidNumber': + case 'ldapGroupMemberAssocAttr': + case 'ldapQuotaAttribute': + case 'ldapEmailAttribute': + case 'ldapUuidUserAttribute': + case 'ldapUuidGroupAttribute': + case 'ldapExpertUsernameAttr': + case 'ldapExpertUUIDUserAttr': + case 'ldapExpertUUIDGroupAttr': + case 'ldapExtStorageHomeAttribute': + case 'ldapAttributePhone': + case 'ldapAttributeWebsite': + case 'ldapAttributeAddress': + case 'ldapAttributeTwitter': + case 'ldapAttributeFediverse': + case 'ldapAttributeOrganisation': + case 'ldapAttributeRole': + case 'ldapAttributeHeadline': + case 'ldapAttributeBiography': + case 'ldapAttributeBirthDate': + case 'ldapAttributeAnniversaryDate': + case 'ldapAttributePronouns': $readMethod = 'getLcValue'; break; case 'ldapUserDisplayName': @@ -416,7 +440,7 @@ class Configuration { protected function getSystemValue(string $varName): string { //FIXME: if another system value is added, softcode the default value - return \OC::$server->getConfig()->getSystemValue($varName, false); + return Server::get(IConfig::class)->getSystemValue($varName, false); } protected function getValue(string $varName): string { @@ -424,8 +448,8 @@ class Configuration { if (is_null($defaults)) { $defaults = $this->getDefaults(); } - return \OC::$server->getConfig()->getAppValue('user_ldap', - $this->configPrefix.$varName, + return Server::get(IConfig::class)->getAppValue('user_ldap', + $this->configPrefix . $varName, $defaults[$varName]); } @@ -453,9 +477,9 @@ class Configuration { } protected function saveValue(string $varName, string $value): bool { - \OC::$server->getConfig()->setAppValue( + Server::get(IConfig::class)->setAppValue( 'user_ldap', - $this->configPrefix.$varName, + $this->configPrefix . $varName, $value ); return true; @@ -463,7 +487,7 @@ class Configuration { /** * @return array an associative array with the default values. Keys are correspond - * to config-value entries in the database table + * to config-value entries in the database table */ public function getDefaults(): array { return [ @@ -538,6 +562,7 @@ class Configuration { 'ldap_admin_group' => '', 'ldap_attr_birthdate' => '', 'ldap_attr_anniversarydate' => '', + 'ldap_attr_pronouns' => '', ]; } @@ -617,6 +642,7 @@ class Configuration { 'ldap_admin_group' => 'ldapAdminGroup', 'ldap_attr_birthdate' => 'ldapAttributeBirthDate', 'ldap_attr_anniversarydate' => 'ldapAttributeAnniversaryDate', + 'ldap_attr_pronouns' => 'ldapAttributePronouns', ]; return $array; } @@ -646,7 +672,7 @@ class Configuration { return [strtolower($attribute)]; } if ($value !== self::AVATAR_PREFIX_DEFAULT) { - \OCP\Server::get(LoggerInterface::class)->warning('Invalid config value to ldapUserAvatarRule; falling back to default.'); + Server::get(LoggerInterface::class)->warning('Invalid config value to ldapUserAvatarRule; falling back to default.'); } return $defaultAttributes; } diff --git a/apps/user_ldap/lib/Connection.php b/apps/user_ldap/lib/Connection.php index c36229f59e5..336179ac341 100644 --- a/apps/user_ldap/lib/Connection.php +++ b/apps/user_ldap/lib/Connection.php @@ -8,6 +8,12 @@ namespace OCA\User_LDAP; use OC\ServerNotAvailableException; +use OCA\User_LDAP\Exceptions\ConfigurationIssueException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IL10N; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; /** @@ -85,11 +91,10 @@ use Psr\Log\LoggerInterface; * @property string $ldapAttributeBiography * @property string $ldapAdminGroup * @property string $ldapAttributeBirthDate + * @property string $ldapAttributePronouns */ class Connection extends LDAPUtility { private ?\LDAP\Connection $ldapConnectionRes = null; - private string $configPrefix; - private ?string $configID; private bool $configured = false; /** @@ -108,11 +113,11 @@ class Connection extends LDAPUtility { public $hasGidNumber = true; /** - * @var \OCP\ICache|null + * @var ICache|null */ protected $cache = null; - /** @var Configuration settings handler **/ + /** @var Configuration settings handler * */ protected $configuration; /** @@ -130,27 +135,30 @@ class Connection extends LDAPUtility { */ protected $bindResult = []; - /** @var LoggerInterface */ - protected $logger; + protected LoggerInterface $logger; + private IL10N $l10n; /** * Constructor * @param string $configPrefix a string with the prefix for the configkey column (appconfig table) * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections */ - public function __construct(ILDAPWrapper $ldap, string $configPrefix = '', ?string $configID = 'user_ldap') { + public function __construct( + ILDAPWrapper $ldap, + private string $configPrefix = '', + private ?string $configID = 'user_ldap', + ) { parent::__construct($ldap); - $this->configPrefix = $configPrefix; - $this->configID = $configID; - $this->configuration = new Configuration($configPrefix, !is_null($configID)); - $memcache = \OC::$server->getMemCacheFactory(); + $this->configuration = new Configuration($this->configPrefix, !is_null($this->configID)); + $memcache = Server::get(ICacheFactory::class); if ($memcache->isAvailable()) { $this->cache = $memcache->createDistributed(); } - $helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); + $helper = Server::get(Helper::class); $this->doNotValidate = !in_array($this->configPrefix, $helper->getServerConfigurationPrefixes()); - $this->logger = \OC::$server->get(LoggerInterface::class); + $this->logger = Server::get(LoggerInterface::class); + $this->l10n = Util::getL10N('user_ldap'); } public function __destruct() { @@ -258,11 +266,11 @@ class Connection extends LDAPUtility { * @param string|null $key */ private function getCacheKey($key): string { - $prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-'; + $prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-'; if (is_null($key)) { return $prefix; } - return $prefix.hash('sha256', $key); + return $prefix . hash('sha256', $key); } /** @@ -313,7 +321,7 @@ class Connection extends LDAPUtility { /** * Caches the general LDAP configuration. * @param bool $force optional. true, if the re-read should be forced. defaults - * to false. + * to false. */ private function readConfiguration(bool $force = false): void { if ((!$this->configured || $force) && !is_null($this->configID)) { @@ -326,16 +334,17 @@ class Connection extends LDAPUtility { * set LDAP configuration with values delivered by an array, not read from configuration * @param array $config array that holds the config parameters in an associated array * @param array &$setParameters optional; array where the set fields will be given to + * @param bool $throw if true, throw ConfigurationIssueException with details instead of returning false * @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters */ - public function setConfiguration($config, &$setParameters = null): bool { + public function setConfiguration(array $config, ?array &$setParameters = null, bool $throw = false): bool { if (is_null($setParameters)) { $setParameters = []; } $this->doNotValidate = false; $this->configuration->setConfiguration($config, $setParameters); if (count($setParameters) > 0) { - $this->configured = $this->validateConfiguration(); + $this->configured = $this->validateConfiguration($throw); } @@ -397,8 +406,7 @@ class Connection extends LDAPUtility { } foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute', - 'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] - as $expertSetting => $effectiveSetting) { + 'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] as $expertSetting => $effectiveSetting) { $uuidOverride = $this->configuration->$expertSetting; if (!empty($uuidOverride)) { $this->configuration->$effectiveSetting = $uuidOverride; @@ -410,7 +418,7 @@ class Connection extends LDAPUtility { $this->configuration->$effectiveSetting = 'auto'; $this->configuration->saveConfiguration(); $this->logger->info( - 'Illegal value for the '.$effectiveSetting.', reset to autodetect.', + 'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.', ['app' => 'user_ldap'] ); } @@ -442,10 +450,10 @@ class Connection extends LDAPUtility { } } - private function doCriticalValidation(): bool { - $configurationOK = true; - $errorStr = 'Configuration Error (prefix ' . $this->configPrefix . '): '; - + /** + * @throws ConfigurationIssueException + */ + private function doCriticalValidation(): void { //options that shall not be empty $options = ['ldapHost', 'ldapUserDisplayName', 'ldapGroupDisplayName', 'ldapLoginFilter']; @@ -478,10 +486,9 @@ class Connection extends LDAPUtility { $subj = $key; break; } - $configurationOK = false; - $this->logger->warning( - $errorStr.'No '.$subj.' given!', - ['app' => 'user_ldap'] + throw new ConfigurationIssueException( + 'No ' . $subj . ' given!', + $this->l10n->t('Mandatory field "%s" left empty', $subj), ); } } @@ -489,47 +496,76 @@ class Connection extends LDAPUtility { //combinations $agent = $this->configuration->ldapAgentName; $pwd = $this->configuration->ldapAgentPassword; - if ( - ($agent === '' && $pwd !== '') - || ($agent !== '' && $pwd === '') - ) { - $this->logger->warning( - $errorStr.'either no password is given for the user ' . - 'agent or a password is given, but not an LDAP agent.', - ['app' => 'user_ldap'] + if ($agent === '' && $pwd !== '') { + throw new ConfigurationIssueException( + 'A password is given, but not an LDAP agent', + $this->l10n->t('A password is given, but not an LDAP agent'), + ); + } + if ($agent !== '' && $pwd === '') { + throw new ConfigurationIssueException( + 'No password is given for the user agent', + $this->l10n->t('No password is given for the user agent'), ); - $configurationOK = false; } $base = $this->configuration->ldapBase; $baseUsers = $this->configuration->ldapBaseUsers; $baseGroups = $this->configuration->ldapBaseGroups; - if (empty($base) && empty($baseUsers) && empty($baseGroups)) { - $this->logger->warning( - $errorStr.'Not a single Base DN given.', - ['app' => 'user_ldap'] + if (empty($base)) { + throw new ConfigurationIssueException( + 'Not a single Base DN given', + $this->l10n->t('No LDAP base DN was given'), ); - $configurationOK = false; } - if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') - === false) { - $this->logger->warning( - $errorStr.'login filter does not contain %uid place holder.', - ['app' => 'user_ldap'] + if (!empty($baseUsers) && !$this->checkBasesAreValid($baseUsers, $base)) { + throw new ConfigurationIssueException( + 'User base is not in root base', + $this->l10n->t('User base DN is not a subnode of global base DN'), ); - $configurationOK = false; } - return $configurationOK; + if (!empty($baseGroups) && !$this->checkBasesAreValid($baseGroups, $base)) { + throw new ConfigurationIssueException( + 'Group base is not in root base', + $this->l10n->t('Group base DN is not a subnode of global base DN'), + ); + } + + if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) { + throw new ConfigurationIssueException( + 'Login filter does not contain %uid placeholder.', + $this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']), + ); + } + } + + /** + * Checks that all bases are subnodes of one of the root bases + */ + private function checkBasesAreValid(array $bases, array $rootBases): bool { + foreach ($bases as $base) { + $ok = false; + foreach ($rootBases as $rootBase) { + if (str_ends_with($base, $rootBase)) { + $ok = true; + break; + } + } + if (!$ok) { + return false; + } + } + return true; } /** * Validates the user specified configuration * @return bool true if configuration seems OK, false otherwise */ - private function validateConfiguration(): bool { + private function validateConfiguration(bool $throw = false): bool { if ($this->doNotValidate) { //don't do a validation if it is a new configuration with pure //default values. Will be allowed on changes via __set or @@ -543,7 +579,19 @@ class Connection extends LDAPUtility { //second step: critical checks. If left empty or filled wrong, mark as //not configured and give a warning. - return $this->doCriticalValidation(); + try { + $this->doCriticalValidation(); + return true; + } catch (ConfigurationIssueException $e) { + if ($throw) { + throw $e; + } + $this->logger->warning( + 'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(), + ['exception' => $e] + ); + return false; + } } @@ -577,19 +625,6 @@ class Connection extends LDAPUtility { return false; } - if ($this->configuration->turnOffCertCheck) { - if (putenv('LDAPTLS_REQCERT=never')) { - $this->logger->debug( - 'Turned off SSL certificate validation successfully.', - ['app' => 'user_ldap'] - ); - } else { - $this->logger->warning( - 'Could not turn off SSL certificate validation.', - ['app' => 'user_ldap'] - ); - } - } $hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== ''); $hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== ''); @@ -613,9 +648,11 @@ class Connection extends LDAPUtility { } } $this->logger->warning( - 'Main LDAP not reachable, connecting to backup', + 'Main LDAP not reachable, connecting to backup: {msg}', [ - 'app' => 'user_ldap' + 'app' => 'user_ldap', + 'msg' => $e->getMessage(), + 'exception' => $e, ] ); } @@ -624,8 +661,8 @@ class Connection extends LDAPUtility { $this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? ''); $this->bindResult = []; $bindStatus = $this->bind(); - $error = $this->ldap->isResource($this->ldapConnectionRes) ? - $this->ldap->errno($this->ldapConnectionRes) : -1; + $error = $this->ldap->isResource($this->ldapConnectionRes) + ? $this->ldap->errno($this->ldapConnectionRes) : -1; if ($bindStatus && $error === 0 && !$forceBackupHost) { //when bind to backup server succeeded and failed to main server, //skip contacting it for 15min @@ -666,6 +703,20 @@ class Connection extends LDAPUtility { } if ($this->configuration->ldapTLS) { + if ($this->configuration->turnOffCertCheck) { + if ($this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) { + $this->logger->debug( + 'Turned off SSL certificate validation successfully.', + ['app' => 'user_ldap'] + ); + } else { + $this->logger->warning( + 'Could not turn off SSL certificate validation.', + ['app' => 'user_ldap'] + ); + } + } + if (!$this->ldap->startTls($this->ldapConnectionRes)) { throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.'); } diff --git a/apps/user_ldap/lib/ConnectionFactory.php b/apps/user_ldap/lib/ConnectionFactory.php index 1e86699c427..dd0ad31920a 100644 --- a/apps/user_ldap/lib/ConnectionFactory.php +++ b/apps/user_ldap/lib/ConnectionFactory.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,11 +7,9 @@ namespace OCA\User_LDAP; class ConnectionFactory { - /** @var ILDAPWrapper */ - private $ldap; - - public function __construct(ILDAPWrapper $ldap) { - $this->ldap = $ldap; + public function __construct( + private ILDAPWrapper $ldap, + ) { } public function get($prefix) { diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php index a2f821dbbda..d98e6d41b52 100644 --- a/apps/user_ldap/lib/Controller/ConfigAPIController.php +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,7 +12,9 @@ use OC\Security\IdentityProof\Manager; use OCA\User_LDAP\Configuration; use OCA\User_LDAP\ConnectionFactory; use OCA\User_LDAP\Helper; +use OCA\User_LDAP\Settings\Admin; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSException; @@ -19,6 +22,7 @@ use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; +use OCP\ServerVersion; use Psr\Log\LoggerInterface; class ConfigAPIController extends OCSController { @@ -29,9 +33,10 @@ class ConfigAPIController extends OCSController { IUserSession $userSession, IUserManager $userManager, Manager $keyManager, + ServerVersion $serverVersion, private Helper $ldapHelper, private LoggerInterface $logger, - private ConnectionFactory $connectionFactory + private ConnectionFactory $connectionFactory, ) { parent::__construct( $appName, @@ -39,19 +44,20 @@ class ConfigAPIController extends OCSController { $capabilitiesManager, $userSession, $userManager, - $keyManager + $keyManager, + $serverVersion, ); } /** * Create a new (empty) configuration and return the resulting prefix * - * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) * @return DataResponse<Http::STATUS_OK, array{configID: string}, array{}> * @throws OCSException * * 200: Config created successfully */ + #[AuthorizedAdminSetting(settings: Admin::class)] public function create() { try { $configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix(); @@ -68,14 +74,14 @@ class ConfigAPIController extends OCSController { /** * Delete a LDAP configuration * - * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) * @param string $configID ID of the config - * @return DataResponse<Http::STATUS_OK, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @throws OCSException * @throws OCSNotFoundException Config not found * * 200: Config deleted successfully */ + #[AuthorizedAdminSetting(settings: Admin::class)] public function delete($configID) { try { $this->ensureConfigIDExists($configID); @@ -95,16 +101,16 @@ class ConfigAPIController extends OCSController { /** * Modify a configuration * - * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) * @param string $configID ID of the config * @param array<string, mixed> $configData New config - * @return DataResponse<Http::STATUS_OK, array<empty>, array{}> + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @throws OCSException * @throws OCSBadRequestException Modifying config is not possible * @throws OCSNotFoundException Config not found * * 200: Config returned */ + #[AuthorizedAdminSetting(settings: Admin::class)] public function modify($configID, $configData) { try { $this->ensureConfigIDExists($configID); @@ -200,7 +206,6 @@ class ConfigAPIController extends OCSController { * </data> * </ocs> * - * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) * @param string $configID ID of the config * @param bool $showPassword Whether to show the password * @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}> @@ -209,6 +214,7 @@ class ConfigAPIController extends OCSController { * * 200: Config returned */ + #[AuthorizedAdminSetting(settings: Admin::class)] public function show($configID, $showPassword = false) { try { $this->ensureConfigIDExists($configID); @@ -237,10 +243,10 @@ class ConfigAPIController extends OCSController { /** * If the given config ID is not available, an exception is thrown * - * @AuthorizedAdminSetting(settings=OCA\User_LDAP\Settings\Admin) * @param string $configID * @throws OCSNotFoundException */ + #[AuthorizedAdminSetting(settings: Admin::class)] private function ensureConfigIDExists($configID): void { $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(); if (!in_array($configID, $prefixes, true)) { diff --git a/apps/user_ldap/lib/Controller/RenewPasswordController.php b/apps/user_ldap/lib/Controller/RenewPasswordController.php index 92b2876dc3f..8389a362b8f 100644 --- a/apps/user_ldap/lib/Controller/RenewPasswordController.php +++ b/apps/user_ldap/lib/Controller/RenewPasswordController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,7 +7,10 @@ namespace OCA\User_LDAP\Controller; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\HintException; @@ -20,17 +24,6 @@ use OCP\IUserManager; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class RenewPasswordController extends Controller { - /** @var IUserManager */ - private $userManager; - /** @var IConfig */ - private $config; - /** @var IL10N */ - protected $l10n; - /** @var ISession */ - private $session; - /** @var IURLGenerator */ - private $urlGenerator; - /** * @param string $appName * @param IRequest $request @@ -38,35 +31,35 @@ class RenewPasswordController extends Controller { * @param IConfig $config * @param IURLGenerator $urlGenerator */ - public function __construct($appName, IRequest $request, IUserManager $userManager, - IConfig $config, IL10N $l10n, ISession $session, IURLGenerator $urlGenerator) { + public function __construct( + $appName, + IRequest $request, + private IUserManager $userManager, + private IConfig $config, + protected IL10N $l10n, + private ISession $session, + private IURLGenerator $urlGenerator, + ) { parent::__construct($appName, $request); - $this->userManager = $userManager; - $this->config = $config; - $this->l10n = $l10n; - $this->session = $session; - $this->urlGenerator = $urlGenerator; } /** - * @PublicPage - * @NoCSRFRequired - * * @return RedirectResponse */ + #[PublicPage] + #[NoCSRFRequired] public function cancel() { return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm')); } /** - * @PublicPage - * @NoCSRFRequired - * @UseSession - * * @param string $user * * @return TemplateResponse|RedirectResponse */ + #[PublicPage] + #[NoCSRFRequired] + #[UseSession] public function showRenewPasswordForm($user) { if ($this->config->getUserValue($user, 'user_ldap', 'needsPasswordReset') !== 'true') { return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm')); @@ -102,15 +95,14 @@ class RenewPasswordController extends Controller { } /** - * @PublicPage - * @UseSession - * * @param string $user * @param string $oldPassword * @param string $newPassword * * @return RedirectResponse */ + #[PublicPage] + #[UseSession] public function tryRenewPassword($user, $oldPassword, $newPassword) { if ($this->config->getUserValue($user, 'user_ldap', 'needsPasswordReset') !== 'true') { return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm')); @@ -127,7 +119,7 @@ class RenewPasswordController extends Controller { try { if (!is_null($newPassword) && \OC_User::setPassword($user, $newPassword)) { $this->session->set('loginMessages', [ - [], [$this->l10n->t("Please login with the new password")] + [], [$this->l10n->t('Please login with the new password')] ]); $this->config->setUserValue($user, 'user_ldap', 'needsPasswordReset', 'false'); return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)); @@ -146,12 +138,11 @@ class RenewPasswordController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * @UseSession - * * @return RedirectResponse */ + #[PublicPage] + #[NoCSRFRequired] + #[UseSession] public function showLoginFormInvalidPassword($user) { $args = !is_null($user) ? ['user' => $user] : []; $this->session->set('loginMessages', [ diff --git a/apps/user_ldap/lib/Events/GroupBackendRegistered.php b/apps/user_ldap/lib/Events/GroupBackendRegistered.php index 273b668946b..a94c239c1b3 100644 --- a/apps/user_ldap/lib/Events/GroupBackendRegistered.php +++ b/apps/user_ldap/lib/Events/GroupBackendRegistered.php @@ -19,14 +19,10 @@ use OCP\EventDispatcher\Event; */ class GroupBackendRegistered extends Event { - /** @var GroupPluginManager */ - private $pluginManager; - /** @var IGroupLDAP */ - private $backend; - - public function __construct(IGroupLDAP $backend, GroupPluginManager $pluginManager) { - $this->pluginManager = $pluginManager; - $this->backend = $backend; + public function __construct( + private IGroupLDAP $backend, + private GroupPluginManager $pluginManager, + ) { } public function getBackend(): IGroupLDAP { diff --git a/apps/user_ldap/lib/Events/UserBackendRegistered.php b/apps/user_ldap/lib/Events/UserBackendRegistered.php index ba824d80d22..a26e23f8f83 100644 --- a/apps/user_ldap/lib/Events/UserBackendRegistered.php +++ b/apps/user_ldap/lib/Events/UserBackendRegistered.php @@ -19,14 +19,10 @@ use OCP\EventDispatcher\Event; */ class UserBackendRegistered extends Event { - /** @var IUserLDAP */ - private $backend; - /** @var UserPluginManager */ - private $pluginManager; - - public function __construct(IUserLDAP $backend, UserPluginManager $pluginManager) { - $this->backend = $backend; - $this->pluginManager = $pluginManager; + public function __construct( + private IUserLDAP $backend, + private UserPluginManager $pluginManager, + ) { } public function getBackend(): IUserLDAP { diff --git a/apps/user_ldap/lib/Exceptions/AttributeNotSet.php b/apps/user_ldap/lib/Exceptions/AttributeNotSet.php index 35df63518b4..4d6053eda66 100644 --- a/apps/user_ldap/lib/Exceptions/AttributeNotSet.php +++ b/apps/user_ldap/lib/Exceptions/AttributeNotSet.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Exceptions/ConfigurationIssueException.php b/apps/user_ldap/lib/Exceptions/ConfigurationIssueException.php new file mode 100644 index 00000000000..efeb426b13d --- /dev/null +++ b/apps/user_ldap/lib/Exceptions/ConfigurationIssueException.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\User_LDAP\Exceptions; + +use OCP\HintException; + +class ConfigurationIssueException extends HintException { +} diff --git a/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php b/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php index f468abd2282..d0d384c31de 100644 --- a/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php +++ b/apps/user_ldap/lib/Exceptions/ConstraintViolationException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Exceptions/NotOnLDAP.php b/apps/user_ldap/lib/Exceptions/NotOnLDAP.php index 8f3d8fc3499..cd74e918829 100644 --- a/apps/user_ldap/lib/Exceptions/NotOnLDAP.php +++ b/apps/user_ldap/lib/Exceptions/NotOnLDAP.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/FilesystemHelper.php b/apps/user_ldap/lib/FilesystemHelper.php deleted file mode 100644 index 2a17282361a..00000000000 --- a/apps/user_ldap/lib/FilesystemHelper.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -/** - * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2016 ownCloud, Inc. - * SPDX-License-Identifier: AGPL-3.0-only - */ -namespace OCA\User_LDAP; - -/** - * @brief wraps around static Nextcloud core methods - */ -class FilesystemHelper { - - /** - * @brief states whether the filesystem was loaded - * @return bool - */ - public function isLoaded() { - return \OC\Files\Filesystem::$loaded; - } - - /** - * @brief initializes the filesystem for the given user - * @param string $uid the Nextcloud username of the user - */ - public function setup($uid) { - \OC_Util::setupFS($uid); - } -} diff --git a/apps/user_ldap/lib/GroupPluginManager.php b/apps/user_ldap/lib/GroupPluginManager.php index 16fdcc5228c..9e8ae6805a4 100644 --- a/apps/user_ldap/lib/GroupPluginManager.php +++ b/apps/user_ldap/lib/GroupPluginManager.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,6 +7,7 @@ namespace OCA\User_LDAP; use OCP\GroupInterface; +use OCP\Server; use Psr\Log\LoggerInterface; class GroupPluginManager { @@ -41,7 +43,7 @@ class GroupPluginManager { foreach ($this->which as $action => $v) { if ((bool)($respondToActions & $action)) { $this->which[$action] = $plugin; - \OCP\Server::get(LoggerInterface::class)->debug("Registered action ".$action." to plugin ".get_class($plugin), ['app' => 'user_ldap']); + Server::get(LoggerInterface::class)->debug('Registered action ' . $action . ' to plugin ' . get_class($plugin), ['app' => 'user_ldap']); } } } diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index ba75df02111..271cc96afbd 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -31,24 +31,19 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis protected CappedMemoryCache $cachedGroupsByMember; /** @var CappedMemoryCache<string[]> $cachedNestedGroups array of groups with gid (DN) as key */ protected CappedMemoryCache $cachedNestedGroups; - protected GroupPluginManager $groupPluginManager; protected LoggerInterface $logger; - protected Access $access; /** * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name */ protected string $ldapGroupMemberAssocAttr; - private IConfig $config; - private IUserManager $ncUserManager; public function __construct( - Access $access, - GroupPluginManager $groupPluginManager, - IConfig $config, - IUserManager $ncUserManager + protected Access $access, + protected GroupPluginManager $groupPluginManager, + private IConfig $config, + private IUserManager $ncUserManager, ) { - $this->access = $access; $filter = $this->access->connection->ldapGroupFilter; $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr; if (!empty($filter) && !empty($gAssoc)) { @@ -58,11 +53,8 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis $this->cachedGroupMembers = new CappedMemoryCache(); $this->cachedGroupsByMember = new CappedMemoryCache(); $this->cachedNestedGroups = new CappedMemoryCache(); - $this->groupPluginManager = $groupPluginManager; $this->logger = Server::get(LoggerInterface::class); $this->ldapGroupMemberAssocAttr = strtolower((string)$gAssoc); - $this->config = $config; - $this->ncUserManager = $ncUserManager; } /** @@ -454,7 +446,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis string $groupDN, string $search = '', ?int $limit = -1, - ?int $offset = 0 + ?int $offset = 0, ): array { try { $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search); @@ -578,7 +570,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis string $groupDN, string $search = '', ?int $limit = -1, - ?int $offset = 0 + ?int $offset = 0, ): array { try { $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); @@ -603,7 +595,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis string $groupDN, string $search = '', int $limit = -1, - int $offset = 0 + int $offset = 0, ): int { try { $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search); @@ -646,6 +638,10 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis return false; } + /** + * @param string $uid + * @return list<string> + */ protected function getCachedGroupsForUserId(string $uid): array { $groupStr = $this->config->getUserValue($uid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), '[]'); return json_decode($groupStr, true) ?? []; @@ -658,7 +654,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis * This function includes groups based on dynamic group membership. * * @param string $uid Name of the user - * @return string[] Group names + * @return list<string> Group names * @throws Exception * @throws ServerNotAvailableException */ @@ -1023,9 +1019,9 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis return $groupUsers; } $search = $this->access->escapeFilterPart($search, true); - $isMemberUid = - ($this->ldapGroupMemberAssocAttr === 'memberuid' || - $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress'); + $isMemberUid + = ($this->ldapGroupMemberAssocAttr === 'memberuid' + || $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress'); //we need to apply the search filter //alternatives that need to be checked: @@ -1182,7 +1178,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis continue; } $name = $item[$this->access->connection->ldapGroupDisplayName][0] ?? null; - $gid = $this->access->dn2groupname($dn, $name); + $gid = $this->access->dn2groupname($dn, $name, false); if (!$gid) { continue; } @@ -1203,10 +1199,10 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis * compared with GroupInterface::CREATE_GROUP etc. */ public function implementsActions($actions): bool { - return (bool)((GroupInterface::COUNT_USERS | - GroupInterface::DELETE_GROUP | - GroupInterface::IS_ADMIN | - $this->groupPluginManager->getImplementedActions()) & $actions); + return (bool)((GroupInterface::COUNT_USERS + | GroupInterface::DELETE_GROUP + | GroupInterface::IS_ADMIN + | $this->groupPluginManager->getImplementedActions()) & $actions); } /** @@ -1258,7 +1254,7 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis if ($ret = $this->groupPluginManager->deleteGroup($gid)) { // Delete group in nextcloud internal db $this->access->getGroupMapper()->unmap($gid); - $this->access->connection->writeToCache("groupExists" . $gid, false); + $this->access->connection->writeToCache('groupExists' . $gid, false); } return $ret; } @@ -1266,17 +1262,17 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis // Getting dn, if false the group is not mapped $dn = $this->access->groupname2dn($gid); if (!$dn) { - throw new Exception('Could not delete unknown group '.$gid.' in LDAP backend.'); + throw new Exception('Could not delete unknown group ' . $gid . ' in LDAP backend.'); } if (!$this->groupExists($gid)) { // The group does not exist in the LDAP, remove the mapping $this->access->getGroupMapper()->unmap($gid); - $this->access->connection->writeToCache("groupExists" . $gid, false); + $this->access->connection->writeToCache('groupExists' . $gid, false); return true; } - throw new Exception('Could not delete existing group '.$gid.' in LDAP backend.'); + throw new Exception('Could not delete existing group ' . $gid . ' in LDAP backend.'); } /** diff --git a/apps/user_ldap/lib/Group_Proxy.php b/apps/user_ldap/lib/Group_Proxy.php index cd35f86db77..f0cdc7a465d 100644 --- a/apps/user_ldap/lib/Group_Proxy.php +++ b/apps/user_ldap/lib/Group_Proxy.php @@ -18,45 +18,24 @@ use OCP\GroupInterface; use OCP\IConfig; use OCP\IUserManager; -class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend, IBatchMethodsBackend, IIsAdminBackend { - private $backends = []; - private ?Group_LDAP $refBackend = null; - private Helper $helper; - private GroupPluginManager $groupPluginManager; - private bool $isSetUp = false; - private IConfig $config; - private IUserManager $ncUserManager; - +/** + * @template-extends Proxy<Group_LDAP> + */ +class Group_Proxy extends Proxy implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend, IBatchMethodsBackend, IIsAdminBackend { public function __construct( - Helper $helper, + private Helper $helper, ILDAPWrapper $ldap, AccessFactory $accessFactory, - GroupPluginManager $groupPluginManager, - IConfig $config, - IUserManager $ncUserManager, + private GroupPluginManager $groupPluginManager, + private IConfig $config, + private IUserManager $ncUserManager, ) { - parent::__construct($ldap, $accessFactory); - $this->helper = $helper; - $this->groupPluginManager = $groupPluginManager; - $this->config = $config; - $this->ncUserManager = $ncUserManager; + parent::__construct($helper, $ldap, $accessFactory); } - protected function setup(): void { - if ($this->isSetUp) { - return; - } - $serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true); - foreach ($serverConfigPrefixes as $configPrefix) { - $this->backends[$configPrefix] = - new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager); - if (is_null($this->refBackend)) { - $this->refBackend = $this->backends[$configPrefix]; - } - } - - $this->isSetUp = true; + protected function newInstance(string $configPrefix): Group_LDAP { + return new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager); } /** @@ -141,7 +120,7 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet * Get all groups a user belongs to * * @param string $uid Name of the user - * @return string[] with group names + * @return list<string> with group names * * This function fetches all groups a user belongs to. It does not check * if the user exists at all. @@ -152,9 +131,7 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet $groups = []; foreach ($this->backends as $backend) { $backendGroups = $backend->getUserGroups($uid); - if (is_array($backendGroups)) { - $groups = array_merge($groups, $backendGroups); - } + $groups = array_merge($groups, $backendGroups); } return array_values(array_unique($groups)); @@ -252,7 +229,7 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet */ public function getGroupsDetails(array $gids): array { if (!($this instanceof IGroupDetailsBackend || $this->implementsActions(GroupInterface::GROUP_DETAILS))) { - throw new \Exception("Should not have been called"); + throw new \Exception('Should not have been called'); } $groupData = []; diff --git a/apps/user_ldap/lib/Handler/ExtStorageConfigHandler.php b/apps/user_ldap/lib/Handler/ExtStorageConfigHandler.php index d4a8f332362..8b63d54aa66 100644 --- a/apps/user_ldap/lib/Handler/ExtStorageConfigHandler.php +++ b/apps/user_ldap/lib/Handler/ExtStorageConfigHandler.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Helper.php b/apps/user_ldap/lib/Helper.php index c9d0cd1773d..d3abf04fd1e 100644 --- a/apps/user_ldap/lib/Helper.php +++ b/apps/user_ldap/lib/Helper.php @@ -9,19 +9,18 @@ namespace OCA\User_LDAP; use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IConfig; +use OCP\IAppConfig; use OCP\IDBConnection; +use OCP\Server; class Helper { - private IConfig $config; - private IDBConnection $connection; /** @var CappedMemoryCache<string> */ protected CappedMemoryCache $sanitizeDnCache; - public function __construct(IConfig $config, - IDBConnection $connection) { - $this->config = $config; - $this->connection = $connection; + public function __construct( + private IAppConfig $appConfig, + private IDBConnection $connection, + ) { $this->sanitizeDnCache = new CappedMemoryCache(10000); } @@ -29,7 +28,7 @@ class Helper { * returns prefixes for each saved LDAP/AD server configuration. * * @param bool $activeConfigurations optional, whether only active configuration shall be - * retrieved, defaults to false + * retrieved, defaults to false * @return array with a list of the available prefixes * * Configuration prefixes are used to set up configurations for n LDAP or @@ -46,21 +45,37 @@ class Helper { * except the default (first) server shall be connected to. * */ - public function getServerConfigurationPrefixes($activeConfigurations = false): array { + public function getServerConfigurationPrefixes(bool $activeConfigurations = false): array { + $all = $this->getAllServerConfigurationPrefixes(); + if (!$activeConfigurations) { + return $all; + } + return array_values(array_filter( + $all, + fn (string $prefix): bool => ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') === '1') + )); + } + + protected function getAllServerConfigurationPrefixes(): array { + $unfilled = ['UNFILLED']; + $prefixes = $this->appConfig->getValueArray('user_ldap', 'configuration_prefixes', $unfilled); + if ($prefixes !== $unfilled) { + return $prefixes; + } + + /* Fallback to browsing key for migration from Nextcloud<32 */ $referenceConfigkey = 'ldap_configuration_active'; $keys = $this->getServersConfig($referenceConfigkey); $prefixes = []; foreach ($keys as $key) { - if ($activeConfigurations && $this->config->getAppValue('user_ldap', $key, '0') !== '1') { - continue; - } - $len = strlen($key) - strlen($referenceConfigkey); $prefixes[] = substr($key, 0, $len); } - asort($prefixes); + sort($prefixes); + + $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes); return $prefixes; } @@ -69,46 +84,45 @@ class Helper { * * determines the host for every configured connection * - * @return array an array with configprefix as keys + * @return array<string,string> an array with configprefix as keys * */ - public function getServerConfigurationHosts() { - $referenceConfigkey = 'ldap_host'; - - $keys = $this->getServersConfig($referenceConfigkey); + public function getServerConfigurationHosts(): array { + $prefixes = $this->getServerConfigurationPrefixes(); + $referenceConfigkey = 'ldap_host'; $result = []; - foreach ($keys as $key) { - $len = strlen($key) - strlen($referenceConfigkey); - $prefix = substr($key, 0, $len); - $result[$prefix] = $this->config->getAppValue('user_ldap', $key); + foreach ($prefixes as $prefix) { + $result[$prefix] = $this->appConfig->getValueString('user_ldap', $prefix . $referenceConfigkey); } return $result; } /** - * return the next available configuration prefix - * - * @return string + * return the next available configuration prefix and register it as used */ - public function getNextServerConfigurationPrefix() { - $serverConnections = $this->getServerConfigurationPrefixes(); - - if (count($serverConnections) === 0) { - return 's01'; + public function getNextServerConfigurationPrefix(): string { + $prefixes = $this->getServerConfigurationPrefixes(); + + if (count($prefixes) === 0) { + $prefix = 's01'; + } else { + sort($prefixes); + $lastKey = array_pop($prefixes); + $lastNumber = (int)str_replace('s', '', $lastKey); + $prefix = 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT); } - sort($serverConnections); - $lastKey = array_pop($serverConnections); - $lastNumber = (int)str_replace('s', '', $lastKey); - return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT); + $prefixes[] = $prefix; + $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', $prefixes); + return $prefix; } private function getServersConfig(string $value): array { $regex = '/' . $value . '$/S'; - $keys = $this->config->getAppKeys('user_ldap'); + $keys = $this->appConfig->getKeys('user_ldap'); $result = []; foreach ($keys as $key) { if (preg_match($regex, $key) === 1) { @@ -126,7 +140,9 @@ class Helper { * @return bool true on success, false otherwise */ public function deleteServerConfiguration($prefix) { - if (!in_array($prefix, self::getServerConfigurationPrefixes())) { + $prefixes = $this->getServerConfigurationPrefixes(); + $index = array_search($prefix, $prefixes); + if ($index === false) { return false; } @@ -145,7 +161,11 @@ class Helper { $query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%'))); } - $deletedRows = $query->execute(); + $deletedRows = $query->executeStatement(); + + unset($prefixes[$index]); + $this->appConfig->setValueArray('user_ldap', 'configuration_prefixes', array_values($prefixes)); + return $deletedRows !== 0; } @@ -153,10 +173,13 @@ class Helper { * checks whether there is one or more disabled LDAP configurations */ public function haveDisabledConfigurations(): bool { - $all = $this->getServerConfigurationPrefixes(false); - $active = $this->getServerConfigurationPrefixes(true); - - return count($all) !== count($active) || count($all) === 0; + $all = $this->getServerConfigurationPrefixes(); + foreach ($all as $prefix) { + if ($this->appConfig->getValueString('user_ldap', $prefix . 'ldap_configuration_active') !== '1') { + return true; + } + } + return false; } /** @@ -271,7 +294,7 @@ class Helper { throw new \Exception('key uid is expected to be set in $param'); } - $userBackend = \OC::$server->get(User_Proxy::class); + $userBackend = Server::get(User_Proxy::class); $uid = $userBackend->loginName2UserName($param['uid']); if ($uid !== false) { $param['uid'] = $uid; diff --git a/apps/user_ldap/lib/IGroupLDAP.php b/apps/user_ldap/lib/IGroupLDAP.php index 764e0c88e64..667eb421004 100644 --- a/apps/user_ldap/lib/IGroupLDAP.php +++ b/apps/user_ldap/lib/IGroupLDAP.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/ILDAPGroupPlugin.php b/apps/user_ldap/lib/ILDAPGroupPlugin.php index d601e3a6548..261b9383dc1 100644 --- a/apps/user_ldap/lib/ILDAPGroupPlugin.php +++ b/apps/user_ldap/lib/ILDAPGroupPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/ILDAPUserPlugin.php b/apps/user_ldap/lib/ILDAPUserPlugin.php index 4638a1a49da..80437bef452 100644 --- a/apps/user_ldap/lib/ILDAPUserPlugin.php +++ b/apps/user_ldap/lib/ILDAPUserPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -58,7 +59,7 @@ interface ILDAPUserPlugin { public function setDisplayName($uid, $displayName); /** - * checks whether the user is allowed to change his avatar in Nextcloud + * 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 */ diff --git a/apps/user_ldap/lib/IUserLDAP.php b/apps/user_ldap/lib/IUserLDAP.php index 6fcd68c0c01..5e8e29c3adf 100644 --- a/apps/user_ldap/lib/IUserLDAP.php +++ b/apps/user_ldap/lib/IUserLDAP.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,14 +9,14 @@ namespace OCA\User_LDAP; interface IUserLDAP { //Functions used by LDAPProvider - + /** * Return access for LDAP interaction. * @param string $uid * @return Access instance of Access for LDAP interaction */ public function getLDAPAccess($uid); - + /** * Return a new LDAP connection for the specified user. * @param string $uid diff --git a/apps/user_ldap/lib/Jobs/CleanUp.php b/apps/user_ldap/lib/Jobs/CleanUp.php index 22c878ed067..76277b43c0b 100644 --- a/apps/user_ldap/lib/Jobs/CleanUp.php +++ b/apps/user_ldap/lib/Jobs/CleanUp.php @@ -10,10 +10,12 @@ namespace OCA\User_LDAP\Jobs; use OCA\User_LDAP\Helper; use OCA\User_LDAP\Mapping\UserMapping; use OCA\User_LDAP\User\DeletedUsersIndex; -use OCA\User_LDAP\User_LDAP; use OCA\User_LDAP\User_Proxy; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Server; /** * Class CleanUp @@ -27,15 +29,12 @@ class CleanUp extends TimedJob { protected $limit; /** @var int $defaultIntervalMin default interval in minutes */ - protected $defaultIntervalMin = 51; + protected $defaultIntervalMin = 60; - /** @var User_LDAP|User_Proxy $userBackend */ - protected $userBackend; - - /** @var \OCP\IConfig $ocConfig */ + /** @var IConfig $ocConfig */ protected $ocConfig; - /** @var \OCP\IDBConnection $db */ + /** @var IDBConnection $db */ protected $db; /** @var Helper $ldapHelper */ @@ -44,20 +43,15 @@ class CleanUp extends TimedJob { /** @var UserMapping */ protected $mapping; - /** @var DeletedUsersIndex */ - protected $dui; - public function __construct( ITimeFactory $timeFactory, - User_Proxy $userBackend, - DeletedUsersIndex $dui + protected User_Proxy $userBackend, + protected DeletedUsersIndex $dui, ) { parent::__construct($timeFactory); - $minutes = \OC::$server->getConfig()->getSystemValue( + $minutes = Server::get(IConfig::class)->getSystemValue( 'ldapUserCleanupInterval', (string)$this->defaultIntervalMin); $this->setInterval((int)$minutes * 60); - $this->userBackend = $userBackend; - $this->dui = $dui; } /** @@ -73,13 +67,13 @@ class CleanUp extends TimedJob { if (isset($arguments['helper'])) { $this->ldapHelper = $arguments['helper']; } else { - $this->ldapHelper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); + $this->ldapHelper = Server::get(Helper::class); } if (isset($arguments['ocConfig'])) { $this->ocConfig = $arguments['ocConfig']; } else { - $this->ocConfig = \OC::$server->getConfig(); + $this->ocConfig = Server::get(IConfig::class); } if (isset($arguments['userBackend'])) { @@ -89,13 +83,13 @@ class CleanUp extends TimedJob { if (isset($arguments['db'])) { $this->db = $arguments['db']; } else { - $this->db = \OC::$server->getDatabaseConnection(); + $this->db = Server::get(IDBConnection::class); } if (isset($arguments['mapping'])) { $this->mapping = $arguments['mapping']; } else { - $this->mapping = \OCP\Server::get(UserMapping::class); + $this->mapping = Server::get(UserMapping::class); } if (isset($arguments['deletedUsersIndex'])) { @@ -185,8 +179,8 @@ class CleanUp extends TimedJob { * @param bool $reset whether the offset should be set to 0 */ public function setOffset(bool $reset = false): void { - $newOffset = $reset ? 0 : - $this->getOffset() + $this->getChunkSize(); + $newOffset = $reset ? 0 + : $this->getOffset() + $this->getChunkSize(); $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', (string)$newOffset); } diff --git a/apps/user_ldap/lib/Jobs/Sync.php b/apps/user_ldap/lib/Jobs/Sync.php index f0122733b62..26888ae96ae 100644 --- a/apps/user_ldap/lib/Jobs/Sync.php +++ b/apps/user_ldap/lib/Jobs/Sync.php @@ -1,8 +1,10 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\User_LDAP\Jobs; use OC\ServerNotAvailableException; @@ -14,6 +16,7 @@ use OCA\User_LDAP\LDAP; use OCA\User_LDAP\Mapping\UserMapping; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IAvatarManager; use OCP\IConfig; use OCP\IDBConnection; @@ -24,44 +27,38 @@ use Psr\Log\LoggerInterface; class Sync extends TimedJob { public const MAX_INTERVAL = 12 * 60 * 60; // 12h public const MIN_INTERVAL = 30 * 60; // 30min - /** @var Helper */ - protected $ldapHelper; - /** @var LDAP */ - protected $ldap; - /** @var UserMapping */ - protected $mapper; - /** @var IConfig */ - protected $config; - /** @var IAvatarManager */ - protected $avatarManager; - /** @var IDBConnection */ - protected $dbc; - /** @var IUserManager */ - protected $ncUserManager; - /** @var LoggerInterface */ - protected $logger; - /** @var IManager */ - protected $notificationManager; - /** @var ConnectionFactory */ - protected $connectionFactory; - /** @var AccessFactory */ - protected $accessFactory; - public function __construct(ITimeFactory $time) { - parent::__construct($time); + protected LDAP $ldap; + + public function __construct( + ITimeFactory $timeFactory, + private IEventDispatcher $dispatcher, + private IConfig $config, + private IDBConnection $dbc, + private IAvatarManager $avatarManager, + private IUserManager $ncUserManager, + private LoggerInterface $logger, + private IManager $notificationManager, + private UserMapping $mapper, + private Helper $ldapHelper, + private ConnectionFactory $connectionFactory, + private AccessFactory $accessFactory, + ) { + parent::__construct($timeFactory); $this->setInterval( - (int)\OC::$server->getConfig()->getAppValue( + (int)$this->config->getAppValue( 'user_ldap', 'background_sync_interval', (string)self::MIN_INTERVAL ) ); + $this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file')); } /** - * updates the interval + * Updates the interval * - * the idea is to adjust the interval depending on the amount of known users + * The idea is to adjust the interval depending on the amount of known users * and the attempt to update each user one day. At most it would run every * 30 minutes, and at least every 12 hours. */ @@ -79,9 +76,8 @@ class Sync extends TimedJob { /** * returns the smallest configured paging size - * @return int */ - protected function getMinPagingSize() { + protected function getMinPagingSize(): int { $configKeys = $this->config->getAppKeys('user_ldap'); $configKeys = array_filter($configKeys, function ($key) { return str_contains($key, 'ldap_paging_size'); @@ -98,10 +94,8 @@ class Sync extends TimedJob { * @param array $argument */ public function run($argument) { - $this->setArgument($argument); - $isBackgroundJobModeAjax = $this->config - ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; + ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; if ($isBackgroundJobModeAjax) { return; } @@ -134,10 +128,10 @@ class Sync extends TimedJob { } /** - * @param array $cycleData + * @param array{offset: int, prefix: string} $cycleData * @return bool whether more results are expected from the same configuration */ - public function runCycle($cycleData) { + public function runCycle(array $cycleData): bool { $connection = $this->connectionFactory->get($cycleData['prefix']); $access = $this->accessFactory->get($connection); $access->setUserMapper($this->mapper); @@ -162,24 +156,22 @@ class Sync extends TimedJob { } /** - * returns the info about the current cycle that should be run, if any, + * Returns the info about the current cycle that should be run, if any, * otherwise null - * - * @return array|null */ - public function getCycle() { + public function getCycle(): ?array { $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true); if (count($prefixes) === 0) { return null; } $cycleData = [ - 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', null), + 'prefix' => $this->config->getAppValue('user_ldap', 'background_sync_prefix', 'none'), 'offset' => (int)$this->config->getAppValue('user_ldap', 'background_sync_offset', '0'), ]; if ( - $cycleData['prefix'] !== null + $cycleData['prefix'] !== 'none' && in_array($cycleData['prefix'], $prefixes) ) { return $cycleData; @@ -191,21 +183,21 @@ class Sync extends TimedJob { /** * Save the provided cycle information in the DB * - * @param array $cycleData + * @param array{prefix: ?string, offset: int} $cycleData */ - public function setCycle(array $cycleData) { + public function setCycle(array $cycleData): void { $this->config->setAppValue('user_ldap', 'background_sync_prefix', $cycleData['prefix']); - $this->config->setAppValue('user_ldap', 'background_sync_offset', $cycleData['offset']); + $this->config->setAppValue('user_ldap', 'background_sync_offset', (string)$cycleData['offset']); } /** * returns data about the next cycle that should run, if any, otherwise * null. It also always goes for the next LDAP configuration! * - * @param array|null $cycleData the old cycle - * @return array|null + * @param ?array{prefix: string, offset: int} $cycleData the old cycle + * @return ?array{prefix: string, offset: int} */ - public function determineNextCycle(?array $cycleData = null) { + public function determineNextCycle(?array $cycleData = null): ?array { $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true); if (count($prefixes) === 0) { return null; @@ -225,13 +217,12 @@ class Sync extends TimedJob { } /** - * Checks whether the provided cycle should be run. Currently only the + * Checks whether the provided cycle should be run. Currently, only the * last configuration change goes into account (at least one hour). * - * @param $cycleData - * @return bool + * @param array{prefix: string} $cycleData */ - public function qualifiesToRun($cycleData) { + public function qualifiesToRun(array $cycleData): bool { $lastChange = (int)$this->config->getAppValue('user_ldap', $cycleData['prefix'] . '_lastChange', '0'); if ((time() - $lastChange) > 60 * 30) { return true; @@ -240,23 +231,20 @@ class Sync extends TimedJob { } /** - * increases the offset of the current cycle for the next run + * Increases the offset of the current cycle for the next run * - * @param $cycleData + * @param array{prefix: string, offset: int} $cycleData */ - protected function increaseOffset($cycleData) { + protected function increaseOffset(array $cycleData): void { $ldapConfig = new Configuration($cycleData['prefix']); $cycleData['offset'] += (int)$ldapConfig->ldapPagingSize; $this->setCycle($cycleData); } /** - * determines the next configuration prefix based on the last one (if any) - * - * @param string|null $lastPrefix - * @return string|null + * Determines the next configuration prefix based on the last one (if any) */ - protected function getNextPrefix($lastPrefix) { + protected function getNextPrefix(?string $lastPrefix): ?string { $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(true); $noOfPrefixes = count($prefixes); if ($noOfPrefixes === 0) { @@ -276,73 +264,9 @@ class Sync extends TimedJob { } /** - * "fixes" DI + * Only used in tests */ - public function setArgument($argument) { - if (isset($argument['config'])) { - $this->config = $argument['config']; - } else { - $this->config = \OC::$server->getConfig(); - } - - if (isset($argument['helper'])) { - $this->ldapHelper = $argument['helper']; - } else { - $this->ldapHelper = new Helper($this->config, \OC::$server->getDatabaseConnection()); - } - - if (isset($argument['ldapWrapper'])) { - $this->ldap = $argument['ldapWrapper']; - } else { - $this->ldap = new LDAP($this->config->getSystemValueString('ldap_log_file')); - } - - if (isset($argument['avatarManager'])) { - $this->avatarManager = $argument['avatarManager']; - } else { - $this->avatarManager = \OC::$server->get(IAvatarManager::class); - } - - if (isset($argument['dbc'])) { - $this->dbc = $argument['dbc']; - } else { - $this->dbc = \OC::$server->getDatabaseConnection(); - } - - if (isset($argument['ncUserManager'])) { - $this->ncUserManager = $argument['ncUserManager']; - } else { - $this->ncUserManager = \OC::$server->getUserManager(); - } - - if (isset($argument['logger'])) { - $this->logger = $argument['logger']; - } else { - $this->logger = \OC::$server->get(LoggerInterface::class); - } - - if (isset($argument['notificationManager'])) { - $this->notificationManager = $argument['notificationManager']; - } else { - $this->notificationManager = \OC::$server->getNotificationManager(); - } - - if (isset($argument['mapper'])) { - $this->mapper = $argument['mapper']; - } else { - $this->mapper = \OCP\Server::get(UserMapping::class); - } - - if (isset($argument['connectionFactory'])) { - $this->connectionFactory = $argument['connectionFactory']; - } else { - $this->connectionFactory = new ConnectionFactory($this->ldap); - } - - if (isset($argument['accessFactory'])) { - $this->accessFactory = $argument['accessFactory']; - } else { - $this->accessFactory = \OCP\Server::get(AccessFactory::class); - } + public function overwritePropertiesForTest(LDAP $ldapWrapper): void { + $this->ldap = $ldapWrapper; } } diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index c29b8857e51..1cf20c4b939 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -11,27 +11,30 @@ use OC\ServerNotAvailableException; use OCA\User_LDAP\DataCollector\LdapDataCollector; use OCA\User_LDAP\Exceptions\ConstraintViolationException; use OCP\IConfig; +use OCP\ILogger; use OCP\Profiler\IProfiler; +use OCP\Server; use Psr\Log\LoggerInterface; class LDAP implements ILDAPWrapper { - protected string $logFile = ''; protected array $curArgs = []; protected LoggerInterface $logger; + protected IConfig $config; private ?LdapDataCollector $dataCollector = null; - public function __construct(string $logFile = '') { - $this->logFile = $logFile; - + public function __construct( + protected string $logFile = '', + ) { /** @var IProfiler $profiler */ - $profiler = \OC::$server->get(IProfiler::class); + $profiler = Server::get(IProfiler::class); if ($profiler->isEnabled()) { $this->dataCollector = new LdapDataCollector(); $profiler->add($this->dataCollector); } - $this->logger = \OCP\Server::get(LoggerInterface::class); + $this->logger = Server::get(LoggerInterface::class); + $this->config = Server::get(IConfig::class); } /** @@ -291,9 +294,24 @@ class LDAP implements ILDAPWrapper { return null; } + /** + * Turn resources into string, and removes potentially problematic cookie string to avoid breaking logfiles + */ + private function sanitizeFunctionParameters(array $args): array { + return array_map(function ($item) { + if ($this->isResource($item)) { + return '(resource)'; + } + if (isset($item[0]['value']['cookie']) && $item[0]['value']['cookie'] !== '') { + $item[0]['value']['cookie'] = '*opaque cookie*'; + } + return $item; + }, $args); + } + private function preFunctionCall(string $functionName, array $args): void { $this->curArgs = $args; - if(strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) { + if (strcasecmp($functionName, 'ldap_bind') === 0 || strcasecmp($functionName, 'ldap_exop_passwd') === 0) { // The arguments are not key value pairs // \OCA\User_LDAP\LDAP::bind passes 3 arguments, the 3rd being the pw // Remove it via direct array access for now, although a better solution could be found mebbe? @@ -301,32 +319,24 @@ class LDAP implements ILDAPWrapper { $args[2] = IConfig::SENSITIVE_VALUE; } - $this->logger->debug('Calling LDAP function {func} with parameters {args}', [ - 'app' => 'user_ldap', - 'func' => $functionName, - 'args' => json_encode($args), - ]); + if ($this->config->getSystemValue('loglevel') === ILogger::DEBUG) { + /* Only running this if debug loglevel is on, to avoid processing parameters on production */ + $this->logger->debug('Calling LDAP function {func} with parameters {args}', [ + 'app' => 'user_ldap', + 'func' => $functionName, + 'args' => $this->sanitizeFunctionParameters($args), + ]); + } if ($this->dataCollector !== null) { - $args = array_map(function ($item) { - if ($this->isResource($item)) { - return '(resource)'; - } - if (isset($item[0]['value']['cookie']) && $item[0]['value']['cookie'] !== "") { - $item[0]['value']['cookie'] = "*opaque cookie*"; - } - return $item; - }, $this->curArgs); - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $this->dataCollector->startLdapRequest($functionName, $args, $backtrace); + $this->dataCollector->startLdapRequest($functionName, $this->sanitizeFunctionParameters($args), $backtrace); } if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { - $args = array_map(fn ($item) => (!$this->isResource($item) ? $item : '(resource)'), $this->curArgs); file_put_contents( $this->logFile, - $functionName . '::' . json_encode($args) . "\n", + $functionName . '::' . json_encode($this->sanitizeFunctionParameters($args)) . "\n", FILE_APPEND ); } @@ -369,7 +379,7 @@ class LDAP implements ILDAPWrapper { /** * Called after an ldap method is run to act on LDAP error if necessary - * @throw \Exception + * @throws \Exception */ private function postFunctionCall(string $functionName): void { if ($this->isResource($this->curArgs[0])) { diff --git a/apps/user_ldap/lib/LDAPProvider.php b/apps/user_ldap/lib/LDAPProvider.php index a6634382fa1..d9750ae3fcf 100644 --- a/apps/user_ldap/lib/LDAPProvider.php +++ b/apps/user_ldap/lib/LDAPProvider.php @@ -11,32 +11,33 @@ use OCA\User_LDAP\User\DeletedUsersIndex; use OCP\IServerContainer; use OCP\LDAP\IDeletionFlagSupport; use OCP\LDAP\ILDAPProvider; +use Psr\Log\LoggerInterface; /** - * LDAP provider for pulic access to the LDAP backend. + * LDAP provider for public access to the LDAP backend. */ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport { private $userBackend; private $groupBackend; private $logger; - private $helper; - private $deletedUsersIndex; /** * Create new LDAPProvider - * @param \OCP\IServerContainer $serverContainer + * @param IServerContainer $serverContainer * @param Helper $helper * @param DeletedUsersIndex $deletedUsersIndex * @throws \Exception if user_ldap app was not enabled */ - public function __construct(IServerContainer $serverContainer, Helper $helper, DeletedUsersIndex $deletedUsersIndex) { - $this->logger = $serverContainer->getLogger(); - $this->helper = $helper; - $this->deletedUsersIndex = $deletedUsersIndex; + public function __construct( + IServerContainer $serverContainer, + private Helper $helper, + private DeletedUsersIndex $deletedUsersIndex, + ) { + $this->logger = $serverContainer->get(LoggerInterface::class); $userBackendFound = false; $groupBackendFound = false; foreach ($serverContainer->getUserManager()->getBackends() as $backend) { - $this->logger->debug('instance '.get_class($backend).' user backend.', ['app' => 'user_ldap']); + $this->logger->debug('instance ' . get_class($backend) . ' user backend.', ['app' => 'user_ldap']); if ($backend instanceof IUserLDAP) { $this->userBackend = $backend; $userBackendFound = true; @@ -44,7 +45,7 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport { } } foreach ($serverContainer->getGroupManager()->getBackends() as $backend) { - $this->logger->debug('instance '.get_class($backend).' group backend.', ['app' => 'user_ldap']); + $this->logger->debug('instance ' . get_class($backend) . ' group backend.', ['app' => 'user_ldap']); if ($backend instanceof IGroupLDAP) { $this->groupBackend = $backend; $groupBackendFound = true; @@ -117,8 +118,8 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport { /** * Sanitize a DN received from the LDAP server. - * @param array $dn the DN in question - * @return array the sanitized DN + * @param array|string $dn the DN in question + * @return array|string the sanitized DN */ public function sanitizeDN($dn) { return $this->helper->sanitizeDN($dn); diff --git a/apps/user_ldap/lib/LDAPProviderFactory.php b/apps/user_ldap/lib/LDAPProviderFactory.php index cf76233c412..8fad9d52206 100644 --- a/apps/user_ldap/lib/LDAPProviderFactory.php +++ b/apps/user_ldap/lib/LDAPProviderFactory.php @@ -12,11 +12,10 @@ use OCP\LDAP\ILDAPProvider; use OCP\LDAP\ILDAPProviderFactory; class LDAPProviderFactory implements ILDAPProviderFactory { - /** * @var IServerContainer */ - private $serverContainer; - - public function __construct(IServerContainer $serverContainer) { - $this->serverContainer = $serverContainer; + public function __construct( + /** * @var IServerContainer */ + private IServerContainer $serverContainer, + ) { } public function getLDAPProvider(): ILDAPProvider { diff --git a/apps/user_ldap/lib/LDAPUtility.php b/apps/user_ldap/lib/LDAPUtility.php index bca6ec37ecf..39b517528e2 100644 --- a/apps/user_ldap/lib/LDAPUtility.php +++ b/apps/user_ldap/lib/LDAPUtility.php @@ -8,13 +8,12 @@ namespace OCA\User_LDAP; abstract class LDAPUtility { - protected ILDAPWrapper $ldap; - /** * constructor, make sure the subclasses call this one! - * @param ILDAPWrapper $ldapWrapper an instance of an ILDAPWrapper + * @param ILDAPWrapper $ldap an instance of an ILDAPWrapper */ - public function __construct(ILDAPWrapper $ldapWrapper) { - $this->ldap = $ldapWrapper; + public function __construct( + protected ILDAPWrapper $ldap, + ) { } } diff --git a/apps/user_ldap/lib/LoginListener.php b/apps/user_ldap/lib/LoginListener.php index c417300b45e..f397f4694d2 100644 --- a/apps/user_ldap/lib/LoginListener.php +++ b/apps/user_ldap/lib/LoginListener.php @@ -42,7 +42,7 @@ class LoginListener implements IEventListener { public function onPostLogin(IUser $user): void { $this->logger->info( - __CLASS__ . ' - {user} postLogin', + self::class . ' - {user} postLogin', [ 'app' => 'user_ldap', 'user' => $user->getUID(), @@ -67,7 +67,7 @@ class LoginListener implements IEventListener { $groupObject = $this->groupManager->get($groupId); if ($groupObject === null) { $this->logger->error( - __CLASS__ . ' - group {group} could not be found (user {user})', + self::class . ' - group {group} could not be found (user {user})', [ 'app' => 'user_ldap', 'user' => $userId, @@ -81,7 +81,7 @@ class LoginListener implements IEventListener { } catch (Exception $e) { if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { $this->logger->error( - __CLASS__ . ' - group {group} membership failed to be added (user {user})', + self::class . ' - group {group} membership failed to be added (user {user})', [ 'app' => 'user_ldap', 'user' => $userId, @@ -96,7 +96,7 @@ class LoginListener implements IEventListener { $this->groupBackend->addRelationshipToCaches($userId, null, $groupId); $this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject)); $this->logger->info( - __CLASS__ . ' - {user} added to {group}', + self::class . ' - {user} added to {group}', [ 'app' => 'user_ldap', 'user' => $userId, @@ -110,7 +110,7 @@ class LoginListener implements IEventListener { } catch (Exception $e) { if ($e->getReason() !== Exception::REASON_DATABASE_OBJECT_NOT_FOUND) { $this->logger->error( - __CLASS__ . ' - group {group} membership failed to be removed (user {user})', + self::class . ' - group {group} membership failed to be removed (user {user})', [ 'app' => 'user_ldap', 'user' => $userId, @@ -125,7 +125,7 @@ class LoginListener implements IEventListener { $groupObject = $this->groupManager->get($groupId); if ($groupObject === null) { $this->logger->error( - __CLASS__ . ' - group {group} could not be found (user {user})', + self::class . ' - group {group} could not be found (user {user})', [ 'app' => 'user_ldap', 'user' => $userId, diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php index c243731eaf7..fa10312a915 100644 --- a/apps/user_ldap/lib/Mapping/AbstractMapping.php +++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php @@ -8,9 +8,10 @@ namespace OCA\User_LDAP\Mapping; use Doctrine\DBAL\Exception; -use Doctrine\DBAL\Platforms\SqlitePlatform; use OCP\DB\IPreparedStatement; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -20,11 +21,6 @@ use Psr\Log\LoggerInterface; */ abstract class AbstractMapping { /** - * @var \OCP\IDBConnection $dbc - */ - protected $dbc; - - /** * returns the DB table name which holds the mappings * * @return string @@ -32,10 +28,11 @@ abstract class AbstractMapping { abstract protected function getTableName(bool $includePrefix = true); /** - * @param \OCP\IDBConnection $dbc + * @param IDBConnection $dbc */ - public function __construct(\OCP\IDBConnection $dbc) { - $this->dbc = $dbc; + public function __construct( + protected IDBConnection $dbc, + ) { } /** @var array caches Names (value) by DN (key) */ @@ -216,7 +213,7 @@ abstract class AbstractMapping { public function getListOfIdsByDn(array $fdns): array { $totalDBParamLimit = 65000; $sliceSize = 1000; - $maxSlices = $this->dbc->getDatabasePlatform() instanceof SqlitePlatform ? 9 : $totalDBParamLimit / $sliceSize; + $maxSlices = $this->dbc->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE ? 9 : $totalDBParamLimit / $sliceSize; $results = []; $slice = 1; @@ -259,7 +256,7 @@ abstract class AbstractMapping { * * @return string[] */ - public function getNamesBySearch(string $search, string $prefixMatch = "", string $postfixMatch = ""): array { + public function getNamesBySearch(string $search, string $prefixMatch = '', string $postfixMatch = ''): array { $statement = $this->dbc->prepare(' SELECT `owncloud_name` FROM `' . $this->getTableName() . '` @@ -333,7 +330,7 @@ abstract class AbstractMapping { */ public function map($fdn, $name, $uuid) { if (mb_strlen($fdn) > 4000) { - \OCP\Server::get(LoggerInterface::class)->error( + Server::get(LoggerInterface::class)->error( 'Cannot map, because the DN exceeds 4000 characters: {dn}', [ 'app' => 'user_ldap', @@ -406,7 +403,7 @@ abstract class AbstractMapping { * @param callable $preCallback * @param callable $postCallback * @return bool true on success, false when at least one row was not - * deleted + * deleted */ public function clearCb(callable $preCallback, callable $postCallback): bool { $picker = $this->dbc->getQueryBuilder(); @@ -434,7 +431,7 @@ abstract class AbstractMapping { $query = $this->dbc->getQueryBuilder(); $query->select($query->func()->count('ldap_dn_hash')) ->from($this->getTableName()); - $res = $query->execute(); + $res = $query->executeQuery(); $count = $res->fetchOne(); $res->closeCursor(); return (int)$count; @@ -445,7 +442,7 @@ abstract class AbstractMapping { $query->select($query->func()->count('ldap_dn_hash')) ->from($this->getTableName()) ->where($query->expr()->like('directory_uuid', $query->createNamedParameter('invalidated_%'))); - $res = $query->execute(); + $res = $query->executeQuery(); $count = $res->fetchOne(); $res->closeCursor(); return (int)$count; diff --git a/apps/user_ldap/lib/Mapping/UserMapping.php b/apps/user_ldap/lib/Mapping/UserMapping.php index 0eeaaef12fd..a030cd0ab52 100644 --- a/apps/user_ldap/lib/Mapping/UserMapping.php +++ b/apps/user_ldap/lib/Mapping/UserMapping.php @@ -20,11 +20,12 @@ use OCP\Support\Subscription\IAssertion; */ class UserMapping extends AbstractMapping { - private IAssertion $assertion; protected const PROV_API_REGEX = '/\/ocs\/v[1-9].php\/cloud\/(groups|users)/'; - public function __construct(IDBConnection $dbc, IAssertion $assertion) { - $this->assertion = $assertion; + public function __construct( + IDBConnection $dbc, + private IAssertion $assertion, + ) { parent::__construct($dbc); } diff --git a/apps/user_ldap/lib/Migration/GroupMappingMigration.php b/apps/user_ldap/lib/Migration/GroupMappingMigration.php index 437bb202bba..7dfb8705770 100644 --- a/apps/user_ldap/lib/Migration/GroupMappingMigration.php +++ b/apps/user_ldap/lib/Migration/GroupMappingMigration.php @@ -14,11 +14,9 @@ use OCP\Migration\SimpleMigrationStep; abstract class GroupMappingMigration extends SimpleMigrationStep { - /** @var IDBConnection */ - private $dbc; - - public function __construct(IDBConnection $dbc) { - $this->dbc = $dbc; + public function __construct( + private IDBConnection $dbc, + ) { } protected function copyGroupMappingData(string $sourceTable, string $destinationTable): void { diff --git a/apps/user_ldap/lib/Migration/RemoveRefreshTime.php b/apps/user_ldap/lib/Migration/RemoveRefreshTime.php index fd414e7e4b7..88ac56ccb84 100644 --- a/apps/user_ldap/lib/Migration/RemoveRefreshTime.php +++ b/apps/user_ldap/lib/Migration/RemoveRefreshTime.php @@ -22,14 +22,10 @@ use OCP\Migration\IRepairStep; */ class RemoveRefreshTime implements IRepairStep { - /** @var IDBConnection */ - private $dbc; - /** @var IConfig */ - private $config; - - public function __construct(IDBConnection $dbc, IConfig $config) { - $this->dbc = $dbc; - $this->config = $config; + public function __construct( + private IDBConnection $dbc, + private IConfig $config, + ) { } public function getName() { @@ -43,6 +39,6 @@ class RemoveRefreshTime implements IRepairStep { $qb->delete('preferences') ->where($qb->expr()->eq('appid', $qb->createNamedParameter('user_ldap'))) ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lastFeatureRefresh'))) - ->execute(); + ->executeStatement(); } } diff --git a/apps/user_ldap/lib/Migration/SetDefaultProvider.php b/apps/user_ldap/lib/Migration/SetDefaultProvider.php index b4d6b5650fb..0bb04438a1d 100644 --- a/apps/user_ldap/lib/Migration/SetDefaultProvider.php +++ b/apps/user_ldap/lib/Migration/SetDefaultProvider.php @@ -16,16 +16,10 @@ use OCP\Migration\IRepairStep; class SetDefaultProvider implements IRepairStep { - /** @var IConfig */ - private $config; - - /** @var Helper */ - private $helper; - - public function __construct(IConfig $config, - Helper $helper) { - $this->config = $config; - $this->helper = $helper; + public function __construct( + private IConfig $config, + private Helper $helper, + ) { } public function getName(): string { diff --git a/apps/user_ldap/lib/Migration/UUIDFix.php b/apps/user_ldap/lib/Migration/UUIDFix.php index 309910cdf0c..e853f3bba66 100644 --- a/apps/user_ldap/lib/Migration/UUIDFix.php +++ b/apps/user_ldap/lib/Migration/UUIDFix.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Migration/UUIDFixGroup.php b/apps/user_ldap/lib/Migration/UUIDFixGroup.php index 4d039a72ebc..3924c91e7ba 100644 --- a/apps/user_ldap/lib/Migration/UUIDFixGroup.php +++ b/apps/user_ldap/lib/Migration/UUIDFixGroup.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Migration/UUIDFixInsert.php b/apps/user_ldap/lib/Migration/UUIDFixInsert.php index 52c52190654..bb92314d93a 100644 --- a/apps/user_ldap/lib/Migration/UUIDFixInsert.php +++ b/apps/user_ldap/lib/Migration/UUIDFixInsert.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,23 +15,12 @@ use OCP\Migration\IRepairStep; class UUIDFixInsert implements IRepairStep { - /** @var IConfig */ - protected $config; - - /** @var UserMapping */ - protected $userMapper; - - /** @var GroupMapping */ - protected $groupMapper; - - /** @var IJobList */ - protected $jobList; - - public function __construct(IConfig $config, UserMapping $userMapper, GroupMapping $groupMapper, IJobList $jobList) { - $this->config = $config; - $this->userMapper = $userMapper; - $this->groupMapper = $groupMapper; - $this->jobList = $jobList; + public function __construct( + protected IConfig $config, + protected UserMapping $userMapper, + protected GroupMapping $groupMapper, + protected IJobList $jobList, + ) { } /** diff --git a/apps/user_ldap/lib/Migration/UUIDFixUser.php b/apps/user_ldap/lib/Migration/UUIDFixUser.php index 0c292a2c2ae..71c3f638095 100644 --- a/apps/user_ldap/lib/Migration/UUIDFixUser.php +++ b/apps/user_ldap/lib/Migration/UUIDFixUser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/user_ldap/lib/Migration/UnsetDefaultProvider.php b/apps/user_ldap/lib/Migration/UnsetDefaultProvider.php index 2b368f5d2c9..025415cf712 100644 --- a/apps/user_ldap/lib/Migration/UnsetDefaultProvider.php +++ b/apps/user_ldap/lib/Migration/UnsetDefaultProvider.php @@ -15,11 +15,9 @@ use OCP\Migration\IRepairStep; class UnsetDefaultProvider implements IRepairStep { - /** @var IConfig */ - private $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private IConfig $config, + ) { } public function getName(): string { diff --git a/apps/user_ldap/lib/Migration/Version1120Date20210917155206.php b/apps/user_ldap/lib/Migration/Version1120Date20210917155206.php index 9b6ebbfc5de..dc3823bf771 100644 --- a/apps/user_ldap/lib/Migration/Version1120Date20210917155206.php +++ b/apps/user_ldap/lib/Migration/Version1120Date20210917155206.php @@ -22,17 +22,11 @@ use Psr\Log\LoggerInterface; class Version1120Date20210917155206 extends SimpleMigrationStep { - /** @var IDBConnection */ - private $dbc; - /** @var IUserManager */ - private $userManager; - /** @var LoggerInterface */ - private $logger; - - public function __construct(IDBConnection $dbc, IUserManager $userManager, LoggerInterface $logger) { - $this->dbc = $dbc; - $this->userManager = $userManager; - $this->logger = $logger; + public function __construct( + private IDBConnection $dbc, + private IUserManager $userManager, + private LoggerInterface $logger, + ) { } public function getName() { diff --git a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php index ff8b113d3e9..2457acd840d 100644 --- a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php +++ b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php @@ -22,16 +22,13 @@ use Psr\Log\LoggerInterface; class Version1130Date20211102154716 extends SimpleMigrationStep { - /** @var IDBConnection */ - private $dbc; - /** @var LoggerInterface */ - private $logger; /** @var string[] */ private $hashColumnAddedToTables = []; - public function __construct(IDBConnection $dbc, LoggerInterface $logger) { - $this->dbc = $dbc; - $this->logger = $logger; + public function __construct( + private IDBConnection $dbc, + private LoggerInterface $logger, + ) { } public function getName() { diff --git a/apps/user_ldap/lib/Migration/Version1141Date20220323143801.php b/apps/user_ldap/lib/Migration/Version1141Date20220323143801.php index 86f10a26c3e..ecedbf1de20 100644 --- a/apps/user_ldap/lib/Migration/Version1141Date20220323143801.php +++ b/apps/user_ldap/lib/Migration/Version1141Date20220323143801.php @@ -18,10 +18,9 @@ use OCP\Migration\SimpleMigrationStep; class Version1141Date20220323143801 extends SimpleMigrationStep { - private IDBConnection $dbc; - - public function __construct(IDBConnection $dbc) { - $this->dbc = $dbc; + public function __construct( + private IDBConnection $dbc, + ) { } /** diff --git a/apps/user_ldap/lib/Notification/Notifier.php b/apps/user_ldap/lib/Notification/Notifier.php index 82050bd2596..0195cb9e65b 100644 --- a/apps/user_ldap/lib/Notification/Notifier.php +++ b/apps/user_ldap/lib/Notification/Notifier.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,17 +9,16 @@ namespace OCA\User_LDAP\Notification; use OCP\L10N\IFactory; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; class Notifier implements INotifier { - /** @var IFactory */ - protected $l10nFactory; - /** * @param IFactory $l10nFactory */ - public function __construct(\OCP\L10N\IFactory $l10nFactory) { - $this->l10nFactory = $l10nFactory; + public function __construct( + protected IFactory $l10nFactory, + ) { } /** @@ -45,12 +45,12 @@ class Notifier implements INotifier { * @param INotification $notification * @param string $languageCode The code of the language that should be used to prepare the notification * @return INotification - * @throws \InvalidArgumentException When the notification was not prepared by a notifier + * @throws UnknownNotificationException When the notification was not prepared by a notifier */ public function prepare(INotification $notification, string $languageCode): INotification { if ($notification->getApp() !== 'user_ldap') { // Not my app => throw - throw new \InvalidArgumentException(); + throw new UnknownNotificationException(); } // Read the language from the notification @@ -60,7 +60,7 @@ class Notifier implements INotifier { // Deal with known subjects case 'pwd_exp_warn_days': $params = $notification->getSubjectParameters(); - $days = (int) $params[0]; + $days = (int)$params[0]; if ($days === 2) { $notification->setParsedSubject($l->t('Your password will expire tomorrow.')); } elseif ($days === 1) { @@ -76,7 +76,7 @@ class Notifier implements INotifier { default: // Unknown subject => Unknown notification => throw - throw new \InvalidArgumentException(); + throw new UnknownNotificationException(); } } } diff --git a/apps/user_ldap/lib/Proxy.php b/apps/user_ldap/lib/Proxy.php index 285427bfd4d..22b2c6617af 100644 --- a/apps/user_ldap/lib/Proxy.php +++ b/apps/user_ldap/lib/Proxy.php @@ -10,28 +10,66 @@ namespace OCA\User_LDAP; use OCA\User_LDAP\Mapping\GroupMapping; use OCA\User_LDAP\Mapping\UserMapping; use OCP\ICache; +use OCP\ICacheFactory; use OCP\Server; +/** + * @template T + */ abstract class Proxy { /** @var array<string,Access> */ private static array $accesses = []; - private ILDAPWrapper $ldap; private ?bool $isSingleBackend = null; private ?ICache $cache = null; - private AccessFactory $accessFactory; + + /** @var T[] */ + protected array $backends = []; + /** @var ?T */ + protected $refBackend = null; + + protected bool $isSetUp = false; public function __construct( - ILDAPWrapper $ldap, - AccessFactory $accessFactory + private Helper $helper, + private ILDAPWrapper $ldap, + private AccessFactory $accessFactory, ) { - $this->ldap = $ldap; - $this->accessFactory = $accessFactory; - $memcache = \OC::$server->getMemCacheFactory(); + $memcache = Server::get(ICacheFactory::class); if ($memcache->isAvailable()) { $this->cache = $memcache->createDistributed(); } } + protected function setup(): void { + if ($this->isSetUp) { + return; + } + + $serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true); + foreach ($serverConfigPrefixes as $configPrefix) { + $this->backends[$configPrefix] = $this->newInstance($configPrefix); + + if (is_null($this->refBackend)) { + $this->refBackend = $this->backends[$configPrefix]; + } + } + + $this->isSetUp = true; + } + + /** + * @return T + */ + abstract protected function newInstance(string $configPrefix): object; + + /** + * @return T + */ + public function getBackend(string $configPrefix): object { + $this->setup(); + return $this->backends[$configPrefix]; + } + private function addAccess(string $configPrefix): void { $userMap = Server::get(UserMapping::class); $groupMap = Server::get(GroupMapping::class); diff --git a/apps/user_ldap/lib/Service/UpdateGroupsService.php b/apps/user_ldap/lib/Service/UpdateGroupsService.php index 4c007f7201f..94f2a7fd4a1 100644 --- a/apps/user_ldap/lib/Service/UpdateGroupsService.php +++ b/apps/user_ldap/lib/Service/UpdateGroupsService.php @@ -90,7 +90,7 @@ class UpdateGroupsService { if ($e->getReason() !== Exception::REASON_DATABASE_OBJECT_NOT_FOUND) { /* If reason is not found something else removed the membership, that’s fine */ $this->logger->error( - __CLASS__ . ' - group {group} membership failed to be removed (user {user})', + self::class . ' - group {group} membership failed to be removed (user {user})', [ 'app' => 'user_ldap', 'user' => $removedUser, @@ -121,7 +121,7 @@ class UpdateGroupsService { if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { /* If reason is unique constraint something else added the membership, that’s fine */ $this->logger->error( - __CLASS__ . ' - group {group} membership failed to be added (user {user})', + self::class . ' - group {group} membership failed to be added (user {user})', [ 'app' => 'user_ldap', 'user' => $addedUser, @@ -167,7 +167,7 @@ class UpdateGroupsService { } catch (Exception $e) { if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { $this->logger->error( - __CLASS__ . ' - group {group} membership failed to be added (user {user})', + self::class . ' - group {group} membership failed to be added (user {user})', [ 'app' => 'user_ldap', 'user' => $user, diff --git a/apps/user_ldap/lib/Settings/Admin.php b/apps/user_ldap/lib/Settings/Admin.php index a1de0a65362..89fb063265b 100644 --- a/apps/user_ldap/lib/Settings/Admin.php +++ b/apps/user_ldap/lib/Settings/Admin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -9,25 +10,22 @@ use OCA\User_LDAP\Configuration; use OCA\User_LDAP\Helper; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; +use OCP\Server; use OCP\Settings\IDelegatedSettings; -use OCP\Template; +use OCP\Template\ITemplateManager; class Admin implements IDelegatedSettings { - /** @var IL10N */ - private $l; - - /** - * @param IL10N $l - */ - public function __construct(IL10N $l) { - $this->l = $l; + public function __construct( + private IL10N $l, + private ITemplateManager $templateManager, + ) { } /** * @return TemplateResponse */ public function getForm() { - $helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); + $helper = Server::get(Helper::class); $prefixes = $helper->getServerConfigurationPrefixes(); if (count($prefixes) === 0) { $newPrefix = $helper->getNextServerConfigurationPrefix(); @@ -39,11 +37,12 @@ class Admin implements IDelegatedSettings { $hosts = $helper->getServerConfigurationHosts(); - $wControls = new Template('user_ldap', 'part.wizardcontrols'); + $wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols'); $wControls = $wControls->fetchPage(); - $sControls = new Template('user_ldap', 'part.settingcontrols'); + $sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols'); $sControls = $sControls->fetchPage(); + $parameters = []; $parameters['serverConfigurationPrefixes'] = $prefixes; $parameters['serverConfigurationHosts'] = $hosts; $parameters['settingControls'] = $sControls; @@ -55,7 +54,7 @@ class Admin implements IDelegatedSettings { } $defaults = $config->getDefaults(); foreach ($defaults as $key => $default) { - $parameters[$key.'_default'] = $default; + $parameters[$key . '_default'] = $default; } return new TemplateResponse('user_ldap', 'settings', $parameters); @@ -70,8 +69,8 @@ class Admin implements IDelegatedSettings { /** * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * * E.g.: 70 */ diff --git a/apps/user_ldap/lib/Settings/Section.php b/apps/user_ldap/lib/Settings/Section.php index 2c8871db9ce..3b95e25513d 100644 --- a/apps/user_ldap/lib/Settings/Section.php +++ b/apps/user_ldap/lib/Settings/Section.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,18 +11,14 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; class Section implements IIconSection { - /** @var IL10N */ - private $l; - /** @var IURLGenerator */ - private $url; - /** * @param IURLGenerator $url * @param IL10N $l */ - public function __construct(IURLGenerator $url, IL10N $l) { - $this->url = $url; - $this->l = $l; + public function __construct( + private IURLGenerator $url, + private IL10N $l, + ) { } /** @@ -46,8 +43,8 @@ class Section implements IIconSection { /** * @return int whether the form should be rather on the top or bottom of - * the settings navigation. The sections are arranged in ascending order of - * the priority values. It is required to return a value between 0 and 99. + * the settings navigation. The sections are arranged in ascending order of + * the priority values. It is required to return a value between 0 and 99. * * E.g.: 70 */ diff --git a/apps/user_ldap/lib/SetupChecks/LdapConnection.php b/apps/user_ldap/lib/SetupChecks/LdapConnection.php index e0e9e6b63d0..ee8c4ddd595 100644 --- a/apps/user_ldap/lib/SetupChecks/LdapConnection.php +++ b/apps/user_ldap/lib/SetupChecks/LdapConnection.php @@ -58,26 +58,26 @@ class LdapConnection implements ISetupCheck { if (!empty($bindFailedConfigurations)) { $output .= $this->l10n->n( 'Binding failed for this LDAP configuration: %s', - 'Binding failed for these LDAP configurations: %s', + 'Binding failed for %n LDAP configurations: %s', count($bindFailedConfigurations), [implode(',', $bindFailedConfigurations)] - )."\n"; + ) . "\n"; } if (!empty($searchFailedConfigurations)) { $output .= $this->l10n->n( 'Searching failed for this LDAP configuration: %s', - 'Searching failed for these LDAP configurations: %s', + 'Searching failed for %n LDAP configurations: %s', count($searchFailedConfigurations), [implode(',', $searchFailedConfigurations)] - )."\n"; + ) . "\n"; } if (!empty($inactiveConfigurations)) { $output .= $this->l10n->n( 'There is an inactive LDAP configuration: %s', - 'There are inactive LDAP configurations: %s', + 'There are %n inactive LDAP configurations: %s', count($inactiveConfigurations), [implode(',', $inactiveConfigurations)] - )."\n"; + ) . "\n"; } if (!empty($bindFailedConfigurations) || !empty($searchFailedConfigurations)) { return SetupResult::error($output); @@ -86,7 +86,7 @@ class LdapConnection implements ISetupCheck { } return SetupResult::success($this->l10n->n( 'Binding and searching works on the configured LDAP connection (%s)', - 'Binding and searching works on all of the configured LDAP connections (%s)', + 'Binding and searching works on all of the %n configured LDAP connections (%s)', count($availableConfigs), [implode(',', $availableConfigs)] )); diff --git a/apps/user_ldap/lib/User/DeletedUsersIndex.php b/apps/user_ldap/lib/User/DeletedUsersIndex.php index 4b5bb26fa96..f57f71a9d47 100644 --- a/apps/user_ldap/lib/User/DeletedUsersIndex.php +++ b/apps/user_ldap/lib/User/DeletedUsersIndex.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only @@ -7,6 +8,7 @@ namespace OCA\User_LDAP\User; use OCA\User_LDAP\Mapping\UserMapping; use OCP\IConfig; +use OCP\PreConditionNotMetException; use OCP\Share\IManager; /** @@ -14,19 +16,13 @@ use OCP\Share\IManager; * @package OCA\User_LDAP */ class DeletedUsersIndex { - protected IConfig $config; - protected UserMapping $mapping; protected ?array $deletedUsers = null; - private IManager $shareManager; public function __construct( - IConfig $config, - UserMapping $mapping, - IManager $shareManager + protected IConfig $config, + protected UserMapping $mapping, + private IManager $shareManager, ) { - $this->config = $config; - $this->mapping = $mapping; - $this->shareManager = $shareManager; } /** @@ -74,7 +70,7 @@ class DeletedUsersIndex { /** * marks a user as deleted * - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ public function markUser(string $ocName): void { if ($this->isUserMarked($ocName)) { diff --git a/apps/user_ldap/lib/User/Manager.php b/apps/user_ldap/lib/User/Manager.php index c76f217a95a..88a001dd965 100644 --- a/apps/user_ldap/lib/User/Manager.php +++ b/apps/user_ldap/lib/User/Manager.php @@ -8,7 +8,6 @@ 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; @@ -27,40 +26,23 @@ use Psr\Log\LoggerInterface; */ 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<User> $usersByDN */ protected CappedMemoryCache $usersByDN; /** @var CappedMemoryCache<User> $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 + protected IConfig $ocConfig, + protected LoggerInterface $logger, + protected IAvatarManager $avatarManager, + protected Image $image, + protected IUserManager $userManager, + protected INotificationManager $notificationManager, + private 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; } /** @@ -77,12 +59,12 @@ class Manager { * property array * @param string $dn the DN of the user * @param string $uid the internal (owncloud) username - * @return \OCA\User_LDAP\User\User + * @return User */ private function createAndCache($dn, $uid) { $this->checkAccess(); $user = new User($uid, $dn, $this->access, $this->ocConfig, - $this->ocFilesystem, clone $this->image, $this->logger, + clone $this->image, $this->logger, $this->avatarManager, $this->userManager, $this->notificationManager); $this->usersByDN[$dn] = $user; @@ -106,6 +88,7 @@ class Manager { /** * @brief checks whether the Access instance has been set * @throws \Exception if Access has not been set + * @psalm-assert !null $this->access * @return null */ private function checkAccess() { @@ -119,13 +102,14 @@ class Manager { * email, displayname, or others. * * @param bool $minimal - optional, set to true to skip attributes with big - * payload + * 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()->ldapExpertUsernameAttr, $this->access->getConnection()->ldapQuotaAttribute, $this->access->getConnection()->ldapEmailAttribute, $this->access->getConnection()->ldapUserDisplayName, @@ -141,6 +125,7 @@ class Manager { $this->access->getConnection()->ldapAttributeHeadline, $this->access->getConnection()->ldapAttributeBiography, $this->access->getConnection()->ldapAttributeBirthDate, + $this->access->getConnection()->ldapAttributePronouns, ]; $homeRule = (string)$this->access->getConnection()->homeFolderNamingRule; @@ -186,7 +171,7 @@ class Manager { /** * creates and returns an instance of OfflineUser for the specified user * @param string $id - * @return \OCA\User_LDAP\User\OfflineUser + * @return OfflineUser */ public function getDeletedUser($id) { return new OfflineUser( @@ -200,7 +185,7 @@ class Manager { /** * @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 + * @return User|OfflineUser|null */ protected function createInstancyByUserName($id) { //most likely a uid. Check whether it is a deleted user @@ -217,7 +202,7 @@ class Manager { /** * @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 + * @return User|OfflineUser|null * @throws \Exception when connection could not be established */ public function get($id) { @@ -237,4 +222,37 @@ class Manager { return $this->createInstancyByUserName($id); } + + /** + * @brief Checks whether a User object by its DN or Nextcloud username exists + * @param string $id the DN or username of the user + * @throws \Exception when connection could not be established + */ + public function exists($id): bool { + $this->checkAccess(); + $this->logger->debug('Checking if {id} exists', ['id' => $id]); + if (isset($this->usersByDN[$id])) { + return true; + } elseif (isset($this->usersByUid[$id])) { + return true; + } + + if ($this->access->stringResemblesDN($id)) { + $this->logger->debug('{id} looks like a dn', ['id' => $id]); + $uid = $this->access->dn2username($id); + if ($uid !== false) { + return true; + } + } + + // Most likely a uid. Check whether it is a deleted user + if ($this->isDeletedUser($id)) { + return true; + } + $dn = $this->access->username2dn($id); + if ($dn !== false) { + return true; + } + return false; + } } diff --git a/apps/user_ldap/lib/User/OfflineUser.php b/apps/user_ldap/lib/User/OfflineUser.php index 24f11a5a1f9..ecaab7188ba 100644 --- a/apps/user_ldap/lib/User/OfflineUser.php +++ b/apps/user_ldap/lib/User/OfflineUser.php @@ -15,10 +15,6 @@ use OCP\Share\IShare; class OfflineUser { /** - * @var string $ocName - */ - protected $ocName; - /** * @var string $dn */ protected $dn; @@ -52,30 +48,19 @@ class OfflineUser { */ protected $hasActiveShares; /** - * @var IConfig $config - */ - protected $config; - /** * @var IDBConnection $db */ protected $db; + /** - * @var \OCA\User_LDAP\Mapping\UserMapping + * @param string $ocName */ - protected $mapping; - /** @var IManager */ - private $shareManager; - public function __construct( - $ocName, - IConfig $config, - UserMapping $mapping, - IManager $shareManager + protected $ocName, + protected IConfig $config, + protected UserMapping $mapping, + private IManager $shareManager, ) { - $this->ocName = $ocName; - $this->config = $config; - $this->mapping = $mapping; - $this->shareManager = $shareManager; } /** diff --git a/apps/user_ldap/lib/User/User.php b/apps/user_ldap/lib/User/User.php index 53e2e2a983d..8f97ec1701f 100644 --- a/apps/user_ldap/lib/User/User.php +++ b/apps/user_ldap/lib/User/User.php @@ -12,17 +12,19 @@ use OC\Accounts\AccountManager; use OCA\User_LDAP\Access; use OCA\User_LDAP\Connection; use OCA\User_LDAP\Exceptions\AttributeNotSet; -use OCA\User_LDAP\FilesystemHelper; use OCA\User_LDAP\Service\BirthdateParserService; use OCP\Accounts\IAccountManager; use OCP\Accounts\PropertyDoesNotExistException; use OCP\IAvatarManager; use OCP\IConfig; use OCP\Image; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; +use OCP\PreConditionNotMetException; use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; /** @@ -31,105 +33,51 @@ use Psr\Log\LoggerInterface; * represents an LDAP user, gets and holds user-specific information from LDAP */ class User { + protected Connection $connection; /** - * @var Access + * @var array<string,1> */ - protected $access; - /** - * @var Connection - */ - protected $connection; - /** - * @var IConfig - */ - protected $config; - /** - * @var FilesystemHelper - */ - protected $fs; - /** - * @var Image - */ - protected $image; - /** - * @var LoggerInterface - */ - protected $logger; - /** - * @var IAvatarManager - */ - protected $avatarManager; - /** - * @var IUserManager - */ - protected $userManager; - /** - * @var INotificationManager - */ - protected $notificationManager; - /** - * @var string - */ - protected $dn; - /** - * @var string - */ - protected $uid; - /** - * @var string[] - */ - protected $refreshedFeatures = []; - /** - * @var string - */ - protected $avatarImage; + protected array $refreshedFeatures = []; + protected string|false|null $avatarImage = null; protected BirthdateParserService $birthdateParser; /** * DB config keys for user preferences + * @var string */ public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; /** * @brief constructor, make sure the subclasses call this one! - * @param string $username the internal username - * @param string $dn the LDAP DN */ - public function __construct($username, $dn, Access $access, - IConfig $config, FilesystemHelper $fs, Image $image, - LoggerInterface $logger, IAvatarManager $avatarManager, IUserManager $userManager, - INotificationManager $notificationManager) { - if ($username === null) { - $logger->error("uid for '$dn' must not be null!", ['app' => 'user_ldap']); - throw new \InvalidArgumentException('uid must not be null!'); - } elseif ($username === '') { + public function __construct( + protected string $uid, + protected string $dn, + protected Access $access, + protected IConfig $config, + protected Image $image, + protected LoggerInterface $logger, + protected IAvatarManager $avatarManager, + protected IUserManager $userManager, + protected INotificationManager $notificationManager, + ) { + if ($uid === '') { $logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']); throw new \InvalidArgumentException('uid must not be an empty string!'); } - - $this->access = $access; - $this->connection = $access->getConnection(); - $this->config = $config; - $this->fs = $fs; - $this->dn = $dn; - $this->uid = $username; - $this->image = $image; - $this->logger = $logger; - $this->avatarManager = $avatarManager; - $this->userManager = $userManager; - $this->notificationManager = $notificationManager; + $this->connection = $this->access->getConnection(); $this->birthdateParser = new BirthdateParserService(); - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); + Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); } /** * marks a user as deleted * - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function markUser() { + public function markUser(): void { $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0'); if ($curValue === '1') { // the user is already marked, do not write to DB again @@ -143,7 +91,7 @@ class User { * processes results from LDAP for attributes as returned by getAttributesToRead() * @param array $ldapEntry the user entry as retrieved from LDAP */ - public function processAttributes($ldapEntry) { + public function processAttributes(array $ldapEntry): void { //Quota $attr = strtolower($this->connection->ldapQuotaAttribute); if (isset($ldapEntry[$attr])) { @@ -180,7 +128,14 @@ class User { //change event that will trigger fetching the display name again $attr = strtolower($this->connection->ldapEmailAttribute); if (isset($ldapEntry[$attr])) { - $this->updateEmail($ldapEntry[$attr][0]); + $mailValue = 0; + for ($x = 0; $x < count($ldapEntry[$attr]); $x++) { + if (filter_var($ldapEntry[$attr][$x], FILTER_VALIDATE_EMAIL)) { + $mailValue = $x; + break; + } + } + $this->updateEmail($ldapEntry[$attr][$mailValue]); } unset($attr); @@ -201,7 +156,7 @@ class User { } //memberOf groups - $cacheKey = 'getMemberOf'.$this->getUsername(); + $cacheKey = 'getMemberOf' . $this->getUsername(); $groups = false; if (isset($ldapEntry['memberof'])) { $groups = $ldapEntry['memberof']; @@ -217,92 +172,92 @@ class User { // check for cached profile data $username = $this->getUsername(); // buffer variable, to save resource - $cacheKey = 'getUserProfile-'.$username; + $cacheKey = 'getUserProfile-' . $username; $profileCached = $this->connection->getFromCache($cacheKey); // honoring profile disabled in config.php and check if user profile was refreshed - if ($this->config->getSystemValueBool('profile.enabled', true) && - ($profileCached === null) && // no cache or TTL not expired - !$this->wasRefreshed('profile')) { + if ($this->config->getSystemValueBool('profile.enabled', true) + && ($profileCached === null) // no cache or TTL not expired + && !$this->wasRefreshed('profile')) { // check current data $profileValues = []; //User Profile Field - Phone number $attr = strtolower($this->connection->ldapAttributePhone); if (!empty($attr)) { // attribute configured - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_PHONE] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_PHONE] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - website $attr = strtolower($this->connection->ldapAttributeWebsite); if (isset($ldapEntry[$attr])) { - $cutPosition = strpos($ldapEntry[$attr][0], " "); + $cutPosition = strpos($ldapEntry[$attr][0], ' '); if ($cutPosition) { // drop appended label - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE] + $profileValues[IAccountManager::PROPERTY_WEBSITE] = substr($ldapEntry[$attr][0], 0, $cutPosition); } else { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE] + $profileValues[IAccountManager::PROPERTY_WEBSITE] = $ldapEntry[$attr][0]; } } elseif (!empty($attr)) { // configured, but not defined - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_WEBSITE] = ""; + $profileValues[IAccountManager::PROPERTY_WEBSITE] = ''; } //User Profile Field - Address $attr = strtolower($this->connection->ldapAttributeAddress); if (isset($ldapEntry[$attr])) { if (str_contains($ldapEntry[$attr][0], '$')) { // basic format conversion from postalAddress syntax to commata delimited - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS] - = str_replace('$', ", ", $ldapEntry[$attr][0]); + $profileValues[IAccountManager::PROPERTY_ADDRESS] + = str_replace('$', ', ', $ldapEntry[$attr][0]); } else { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS] + $profileValues[IAccountManager::PROPERTY_ADDRESS] = $ldapEntry[$attr][0]; } } elseif (!empty($attr)) { // configured, but not defined - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ADDRESS] = ""; + $profileValues[IAccountManager::PROPERTY_ADDRESS] = ''; } //User Profile Field - Twitter $attr = strtolower($this->connection->ldapAttributeTwitter); if (!empty($attr)) { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_TWITTER] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_TWITTER] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - fediverse $attr = strtolower($this->connection->ldapAttributeFediverse); if (!empty($attr)) { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_FEDIVERSE] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_FEDIVERSE] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - organisation $attr = strtolower($this->connection->ldapAttributeOrganisation); if (!empty($attr)) { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ORGANISATION] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_ORGANISATION] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - role $attr = strtolower($this->connection->ldapAttributeRole); if (!empty($attr)) { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_ROLE] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_ROLE] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - headline $attr = strtolower($this->connection->ldapAttributeHeadline); if (!empty($attr)) { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_HEADLINE] - = $ldapEntry[$attr][0] ?? ""; + $profileValues[IAccountManager::PROPERTY_HEADLINE] + = $ldapEntry[$attr][0] ?? ''; } //User Profile Field - biography $attr = strtolower($this->connection->ldapAttributeBiography); if (isset($ldapEntry[$attr])) { if (str_contains($ldapEntry[$attr][0], '\r')) { // convert line endings - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY] + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = str_replace(["\r\n","\r"], "\n", $ldapEntry[$attr][0]); } else { - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY] + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = $ldapEntry[$attr][0]; } } elseif (!empty($attr)) { // configured, but not defined - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIOGRAPHY] = ""; + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = ''; } //User Profile Field - birthday $attr = strtolower($this->connection->ldapAttributeBirthDate); @@ -310,8 +265,8 @@ class User { $value = $ldapEntry[$attr][0]; try { $birthdate = $this->birthdateParser->parseBirthdate($value); - $profileValues[\OCP\Accounts\IAccountManager::PROPERTY_BIRTHDATE] - = $birthdate->format("Y-m-d"); + $profileValues[IAccountManager::PROPERTY_BIRTHDATE] + = $birthdate->format('Y-m-d'); } catch (InvalidArgumentException $e) { // Invalid date -> just skip the property $this->logger->info("Failed to parse user's birthdate from LDAP: $value", [ @@ -320,6 +275,12 @@ class User { ]); } } + //User Profile Field - pronouns + $attr = strtolower($this->connection->ldapAttributePronouns); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_PRONOUNS] + = $ldapEntry[$attr][0] ?? ''; + } // check for changed data and cache just for TTL checking $checksum = hash('sha256', json_encode($profileValues)); $this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space @@ -330,11 +291,11 @@ class User { $this->updateProfile($profileValues); $this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']); } else { - $this->logger->debug("profile data from LDAP unchanged", ['app' => 'user_ldap', 'uid' => $username]); + $this->logger->debug('profile data from LDAP unchanged', ['app' => 'user_ldap', 'uid' => $username]); } unset($attr); } elseif ($profileCached !== null) { // message delayed, to declutter log - $this->logger->debug("skipping profile check, while cached data exist", ['app' => 'user_ldap', 'uid' => $username]); + $this->logger->debug('skipping profile check, while cached data exist', ['app' => 'user_ldap', 'uid' => $username]); } //Avatar @@ -344,11 +305,7 @@ class User { foreach ($attributes as $attribute) { if (isset($ldapEntry[$attribute])) { $this->avatarImage = $ldapEntry[$attribute][0]; - // the call to the method that saves the avatar in the file - // system must be postponed after the login. It is to ensure - // external mounts are mounted properly (e.g. with login - // credentials from the session). - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin'); + $this->updateAvatar(); break; } } @@ -372,11 +329,9 @@ class User { /** * returns the home directory of the user if specified by LDAP settings - * @param ?string $valueFromLDAP - * @return false|string * @throws \Exception */ - public function getHomePath($valueFromLDAP = null) { + public function getHomePath(?string $valueFromLDAP = null): string|false { $path = (string)$valueFromLDAP; $attr = null; @@ -384,8 +339,12 @@ class User { && str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:') && $this->access->connection->homeFolderNamingRule !== 'attr:') { $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); - $homedir = $this->access->readAttribute($this->access->username2dn($this->getUsername()), $attr); - if ($homedir && isset($homedir[0])) { + $dn = $this->access->username2dn($this->getUsername()); + if ($dn === false) { + return false; + } + $homedir = $this->access->readAttribute($dn, $attr); + if ($homedir !== false && isset($homedir[0])) { $path = $homedir[0]; } } @@ -398,7 +357,7 @@ class User { && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/')) ) { $path = $this->config->getSystemValue('datadirectory', - \OC::$SERVERROOT.'/data') . '/' . $path; + \OC::$SERVERROOT . '/data') . '/' . $path; } //we need it to store it in the DB as well in case a user gets //deleted so we can clean up afterwards @@ -420,8 +379,8 @@ class User { return false; } - public function getMemberOfGroups() { - $cacheKey = 'getMemberOf'.$this->getUsername(); + public function getMemberOfGroups(): array|false { + $cacheKey = 'getMemberOf' . $this->getUsername(); $memberOfGroups = $this->connection->getFromCache($cacheKey); if (!is_null($memberOfGroups)) { return $memberOfGroups; @@ -433,9 +392,9 @@ class User { /** * @brief reads the image from LDAP that shall be used as Avatar - * @return string data (provided by LDAP) | false + * @return string|false data (provided by LDAP) */ - public function getAvatarImage() { + public function getAvatarImage(): string|false { if (!is_null($this->avatarImage)) { return $this->avatarImage; } @@ -446,7 +405,7 @@ class User { $attributes = $connection->resolveRule('avatar'); foreach ($attributes as $attribute) { $result = $this->access->readAttribute($this->dn, $attribute); - if ($result !== false && is_array($result) && isset($result[0])) { + if ($result !== false && isset($result[0])) { $this->avatarImage = $result[0]; break; } @@ -457,20 +416,16 @@ class User { /** * @brief marks the user as having logged in at least once - * @return null */ - public function markLogin() { + public function markLogin(): void { $this->config->setUserValue( $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1'); } /** * Stores a key-value pair in relation to this user - * - * @param string $key - * @param string $value */ - private function store($key, $value) { + private function store(string $key, string $value): void { $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); } @@ -478,12 +433,9 @@ class User { * Composes the display name and stores it in the database. The final * display name is returned. * - * @param string $displayName - * @param string $displayName2 * @return string the effective display name */ - public function composeAndStoreDisplayName($displayName, $displayName2 = '') { - $displayName2 = (string)$displayName2; + public function composeAndStoreDisplayName(string $displayName, string $displayName2 = ''): string { if ($displayName2 !== '') { $displayName .= ' (' . $displayName2 . ')'; } @@ -502,9 +454,8 @@ class User { /** * Stores the LDAP Username in the Database - * @param string $userName */ - public function storeLDAPUserName($userName) { + public function storeLDAPUserName(string $userName): void { $this->store('uid', $userName); } @@ -513,9 +464,8 @@ class User { * already. If not, it will marked like this, because it is expected that * the method will be run, when false is returned. * @param string $feature email | quota | avatar | profile (can be extended) - * @return bool */ - private function wasRefreshed($feature) { + private function wasRefreshed(string $feature): bool { if (isset($this->refreshedFeatures[$feature])) { return true; } @@ -525,10 +475,9 @@ class User { /** * fetches the email from LDAP and stores it as Nextcloud user value - * @param string $valueFromLDAP if known, to save an LDAP read request - * @return null + * @param ?string $valueFromLDAP if known, to save an LDAP read request */ - public function updateEmail($valueFromLDAP = null) { + public function updateEmail(?string $valueFromLDAP = null): void { if ($this->wasRefreshed('email')) { return; } @@ -547,7 +496,7 @@ class User { if (!is_null($user)) { $currentEmail = (string)$user->getSystemEMailAddress(); if ($currentEmail !== $email) { - $user->setEMailAddress($email); + $user->setSystemEMailAddress($email); } } } @@ -566,14 +515,13 @@ class User { * fetch all the user's attributes in one call and use the fetched values in this function. * The expected value for that parameter is a string describing the quota for the user. Valid * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in - * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info) + * bytes), '1234 MB' (quota in MB - check the \OCP\Util::computerFileSize method for more info) * * fetches the quota from LDAP and stores it as Nextcloud user value * @param ?string $valueFromLDAP the quota attribute's value can be passed, - * to save the readAttribute request - * @return void + * to save the readAttribute request */ - public function updateQuota($valueFromLDAP = null) { + public function updateQuota(?string $valueFromLDAP = null): void { if ($this->wasRefreshed('quota')) { return; } @@ -587,7 +535,7 @@ class User { $quota = false; if (is_null($valueFromLDAP) && $quotaAttribute !== '') { $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); - if ($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) { + if ($aQuota !== false && isset($aQuota[0]) && $this->verifyQuotaValue($aQuota[0])) { $quota = $aQuota[0]; } elseif (is_array($aQuota) && isset($aQuota[0])) { $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']); @@ -595,7 +543,7 @@ class User { } elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) { $quota = $valueFromLDAP; } else { - $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ['app' => 'user_ldap']); + $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . ($valueFromLDAP ?? '') . ']', ['app' => 'user_ldap']); } if ($quota === false && $this->verifyQuotaValue($defaultQuota)) { @@ -614,8 +562,8 @@ class User { } } - private function verifyQuotaValue(string $quotaValue) { - return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false; + private function verifyQuotaValue(string $quotaValue): bool { + return $quotaValue === 'none' || $quotaValue === 'default' || Util::computerFileSize($quotaValue) !== false; } /** @@ -631,7 +579,7 @@ class User { // fetch/prepare user $user = $this->userManager->get($this->uid); if (is_null($user)) { - $this->logger->error('could not get user for uid='.$this->uid.'', ['app' => 'user_ldap']); + $this->logger->error('could not get user for uid=' . $this->uid . '', ['app' => 'user_ldap']); return; } // prepare AccountManager and Account @@ -646,41 +594,29 @@ class User { try { $accountProperty = $account->getProperty($property); $currentValue = $accountProperty->getValue(); - $scope = ($accountProperty->getScope() ? $accountProperty->getScope() - : $defaultScopes[$property]); + $scope = ($accountProperty->getScope() ?: $defaultScopes[$property]); } catch (PropertyDoesNotExistException $e) { // thrown at getProperty - $this->logger->error('property does not exist: '.$property - .' for uid='.$this->uid.'', ['app' => 'user_ldap', 'exception' => $e]); + $this->logger->error('property does not exist: ' . $property + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap', 'exception' => $e]); $currentValue = ''; $scope = $defaultScopes[$property]; } - $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what he put there + $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what they put there if ($currentValue !== $value) { $account->setProperty($property, $value, $scope, $verified); - $this->logger->debug('update user profile: '.$property.'='.$value - .' for uid='.$this->uid.'', ['app' => 'user_ldap']); + $this->logger->debug('update user profile: ' . $property . '=' . $value + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap']); } } try { $accountManager->updateAccount($account); // may throw InvalidArgumentException } catch (\InvalidArgumentException $e) { - $this->logger->error('invalid data from LDAP: for uid='.$this->uid.'', ['app' => 'user_ldap', 'func' => 'updateProfile' + $this->logger->error('invalid data from LDAP: for uid=' . $this->uid . '', ['app' => 'user_ldap', 'func' => 'updateProfile' , 'exception' => $e]); } } /** - * called by a post_login hook to save the avatar picture - * - * @param array $params - */ - public function updateAvatarPostLogin($params) { - if (isset($params['uid']) && $params['uid'] === $this->getUsername()) { - $this->updateAvatar(); - } - } - - /** * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar * @return bool true when the avatar was set successfully or is up to date */ @@ -705,7 +641,7 @@ class User { return true; } - $isSet = $this->setOwnCloudAvatar(); + $isSet = $this->setNextcloudAvatar(); if ($isSet) { // save checksum only after successful setting @@ -726,11 +662,10 @@ class User { /** * @brief sets an image as Nextcloud avatar - * @return bool */ - private function setOwnCloudAvatar() { + private function setNextcloudAvatar(): bool { if (!$this->image->valid()) { - $this->logger->error('avatar image data from LDAP invalid for '.$this->dn, ['app' => 'user_ldap']); + $this->logger->error('avatar image data from LDAP invalid for ' . $this->dn, ['app' => 'user_ldap']); return false; } @@ -738,14 +673,10 @@ class User { //make sure it is a square and not bigger than 512x512 $size = min([$this->image->width(), $this->image->height(), 512]); if (!$this->image->centerCrop($size)) { - $this->logger->error('croping image for avatar failed for '.$this->dn, ['app' => 'user_ldap']); + $this->logger->error('croping image for avatar failed for ' . $this->dn, ['app' => 'user_ldap']); return false; } - if (!$this->fs->isLoaded()) { - $this->fs->setup($this->uid); - } - try { $avatar = $this->avatarManager->getAvatar($this->uid); $avatar->set($this->image); @@ -759,7 +690,7 @@ class User { /** * @throws AttributeNotSet * @throws \OC\ServerNotAvailableException - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ public function getExtStorageHome():string { $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', ''); @@ -778,7 +709,7 @@ class User { } /** - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException * @throws \OC\ServerNotAvailableException */ public function updateExtStorageHome(?string $valueFromLDAP = null):string { @@ -787,7 +718,7 @@ class User { } else { $extHomeValues = [$valueFromLDAP]; } - if ($extHomeValues && isset($extHomeValues[0])) { + if ($extHomeValues !== false && isset($extHomeValues[0])) { $extHome = $extHomeValues[0]; $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome); return $extHome; @@ -799,29 +730,30 @@ class User { /** * called by a post_login hook to handle password expiry - * - * @param array $params */ - public function handlePasswordExpiry($params) { + public function handlePasswordExpiry(array $params): void { $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { - return;//password expiry handling disabled + //password expiry handling disabled + return; } $uid = $params['uid']; if (isset($uid) && $uid === $this->getUsername()) { //retrieve relevant user attributes $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); - if (array_key_exists('pwdpolicysubentry', $result[0])) { - $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; - if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { - $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN + if (!empty($result)) { + if (array_key_exists('pwdpolicysubentry', $result[0])) { + $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; + if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { + $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN + } } - } - $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; - $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; - $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; + $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; + $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; + $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; + } //retrieve relevant password policy attributes $cacheKey = 'ppolicyAttributes' . $ppolicyDN; @@ -840,18 +772,18 @@ class User { if (!empty($pwdGraceAuthNLimit) && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available? $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); } else { //no more grace login available - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid])); } exit(); } //handle pwdReset attribute - if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change his password + if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change their password $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); exit(); } @@ -863,7 +795,7 @@ class User { $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) { $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); - $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S')); + $pwdChangedTimeDt->add(new \DateInterval('PT' . $pwdMaxAgeInt . 'S')); $currentDateTime = new \DateTime(); $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); if ($secondsToExpiry <= $pwdExpireWarningInt) { @@ -880,7 +812,7 @@ class User { ->setUser($uid) ->setDateTime($currentDateTime) ->setObject('pwd_exp_warn', $uid) - ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)]) + ->setSubject('pwd_exp_warn_days', [(int)ceil($secondsToExpiry / 60 / 60 / 24)]) ; $this->notificationManager->notify($notification); } diff --git a/apps/user_ldap/lib/UserPluginManager.php b/apps/user_ldap/lib/UserPluginManager.php index 70e477946b8..ed87fea6fde 100644 --- a/apps/user_ldap/lib/UserPluginManager.php +++ b/apps/user_ldap/lib/UserPluginManager.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,6 +7,7 @@ namespace OCA\User_LDAP; use OC\User\Backend; +use OCP\Server; use Psr\Log\LoggerInterface; class UserPluginManager { @@ -43,12 +45,12 @@ class UserPluginManager { foreach ($this->which as $action => $v) { if (is_int($action) && (bool)($respondToActions & $action)) { $this->which[$action] = $plugin; - \OCP\Server::get(LoggerInterface::class)->debug("Registered action ".$action." to plugin ".get_class($plugin), ['app' => 'user_ldap']); + Server::get(LoggerInterface::class)->debug('Registered action ' . $action . ' to plugin ' . get_class($plugin), ['app' => 'user_ldap']); } } if (method_exists($plugin, 'deleteUser')) { $this->which['deleteUser'] = $plugin; - \OCP\Server::get(LoggerInterface::class)->debug("Registered action deleteUser to plugin ".get_class($plugin), ['app' => 'user_ldap']); + Server::get(LoggerInterface::class)->debug('Registered action deleteUser to plugin ' . get_class($plugin), ['app' => 'user_ldap']); } } @@ -95,7 +97,7 @@ class UserPluginManager { } /** - * checks whether the user is allowed to change his avatar in Nextcloud + * 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 diff --git a/apps/user_ldap/lib/User_LDAP.php b/apps/user_ldap/lib/User_LDAP.php index 245205f992d..c3f56f5ff9b 100644 --- a/apps/user_ldap/lib/User_LDAP.php +++ b/apps/user_ldap/lib/User_LDAP.php @@ -14,42 +14,27 @@ 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\IConfig; use OCP\IUserBackend; -use OCP\IUserSession; use OCP\Notification\IManager as INotificationManager; use OCP\User\Backend\ICountMappedUsersBackend; -use OCP\User\Backend\ICountUsersBackend; +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, ICountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend { - protected IConfig $ocConfig; - protected INotificationManager $notificationManager; - protected UserPluginManager $userPluginManager; - protected LoggerInterface $logger; - protected DeletedUsersIndex $deletedUsersIndex; - +class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend { public function __construct( Access $access, - IConfig $ocConfig, - INotificationManager $notificationManager, - IUserSession $userSession, - UserPluginManager $userPluginManager, - LoggerInterface $logger, - DeletedUsersIndex $deletedUsersIndex, + protected INotificationManager $notificationManager, + protected UserPluginManager $userPluginManager, + protected LoggerInterface $logger, + protected DeletedUsersIndex $deletedUsersIndex, ) { parent::__construct($access); - $this->ocConfig = $ocConfig; - $this->notificationManager = $notificationManager; - $this->userPluginManager = $userPluginManager; - $this->logger = $logger; - $this->deletedUsersIndex = $deletedUsersIndex; } /** - * checks whether the user is allowed to change his avatar in Nextcloud + * 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 @@ -82,11 +67,12 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I * @return string|false * @throws \Exception */ - public function loginName2UserName($loginName) { + public function loginName2UserName($loginName, bool $forceLdapRefetch = false) { $cacheKey = 'loginName2UserName-' . $loginName; $username = $this->access->connection->getFromCache($cacheKey); - if ($username !== null) { + $ignoreCache = ($username === false && $forceLdapRefetch); + if ($username !== null && !$ignoreCache) { return $username; } @@ -101,6 +87,9 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I } $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); @@ -130,8 +119,8 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I $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); + throw new NotOnLDAP('No user available for the given login name on ' + . $this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort); } return $users[0]; } @@ -144,22 +133,17 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I * @return false|string */ public function checkPassword($uid, $password) { - try { - $ldapRecord = $this->getLDAPUserByLoginName($uid); - } catch (NotOnLDAP $e) { - $this->logger->debug( - $e->getMessage(), - ['app' => 'user_ldap', 'exception' => $e] - ); + $username = $this->loginName2UserName($uid, true); + if ($username === false) { return false; } - $dn = $ldapRecord['dn'][0]; + $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?', + '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; @@ -171,7 +155,6 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I } $this->access->cacheUserExists($user->getUsername()); - $user->processAttributes($ldapRecord); $user->markLogin(); return $user->getUsername(); @@ -194,8 +177,8 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I $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?'); + 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; @@ -225,7 +208,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I */ public function getUsers($search = '', $limit = 10, $offset = 0) { $search = $this->access->escapeFilterPart($search, true); - $cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset; + $cachekey = 'getUsers-' . $search . '-' . $limit . '-' . $offset; //check if users are cached, if so return $ldap_users = $this->access->connection->getFromCache($cachekey); @@ -245,7 +228,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I ]); $this->logger->debug( - 'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter, + 'getUsers: Options: search ' . $search . ' limit ' . $limit . ' offset ' . $offset . ' Filter: ' . $filter, ['app' => 'user_ldap'] ); //do the search and translate results to Nextcloud names @@ -255,7 +238,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I $limit, $offset); $ldap_users = $this->access->nextcloudUserNames($ldap_users); $this->logger->debug( - 'getUsers: '.count($ldap_users). ' Users found', + 'getUsers: ' . count($ldap_users) . ' Users found', ['app' => 'user_ldap'] ); @@ -266,8 +249,8 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I /** * checks whether a user is still available on LDAP * - * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user - * name or an instance of that user + * @param string|User $user either the Nextcloud user + * name or an instance of that user * @throws \Exception * @throws \OC\ServerNotAvailableException */ @@ -326,23 +309,22 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I * @throws \Exception when connection could not be established */ public function userExists($uid) { - $userExists = $this->access->connection->getFromCache('userExists'.$uid); + $userExists = $this->access->connection->getFromCache('userExists' . $uid); if (!is_null($userExists)) { return (bool)$userExists; } - //getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking. - $user = $this->access->userManager->get($uid); + $userExists = $this->access->userManager->exists($uid); - if (is_null($user)) { + if (!$userExists) { $this->logger->debug( - 'No DN found for '.$uid.' on '.$this->access->connection->ldapHost, + 'No DN found for ' . $uid . ' on ' . $this->access->connection->ldapHost, ['app' => 'user_ldap'] ); - $this->access->connection->writeToCache('userExists'.$uid, false); + $this->access->connection->writeToCache('userExists' . $uid, false); return false; } - $this->access->connection->writeToCache('userExists'.$uid, true); + $this->access->connection->writeToCache('userExists' . $uid, true); return true; } @@ -376,7 +358,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I } if (!$marked) { $this->logger->notice( - 'User '.$uid . ' is not marked as deleted, not cleaning up.', + 'User ' . $uid . ' is not marked as deleted, not cleaning up.', ['app' => 'user_ldap'] ); return false; @@ -409,7 +391,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I return $this->userPluginManager->getHome($uid); } - $cacheKey = 'getHome'.$uid; + $cacheKey = 'getHome' . $uid; $path = $this->access->connection->getFromCache($cacheKey); if (!is_null($path)) { return $path; @@ -441,7 +423,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I return false; } - $cacheKey = 'getDisplayName'.$uid; + $cacheKey = 'getDisplayName' . $uid; if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) { return $displayName; } @@ -468,11 +450,10 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I $user = $this->access->userManager->get($uid); if ($user instanceof User) { - $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2); + $displayName = $user->composeAndStoreDisplayName($displayName, (string)$displayName2); $this->access->connection->writeToCache($cacheKey, $displayName); } if ($user instanceof OfflineUser) { - /** @var OfflineUser $user*/ $displayName = $user->getDisplayName(); } return $displayName; @@ -505,7 +486,7 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I * @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; + $cacheKey = 'getDisplayNames-' . $search . '-' . $limit . '-' . $offset; if (!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) { return $displayNames; } @@ -547,20 +528,18 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I /** * counts the users in LDAP - * - * @return int|false */ - public function countUsers() { + 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; + $cacheKey = 'countUsers-' . $filter . '-' . $limit; if (!is_null($entries = $this->access->connection->getFromCache($cacheKey))) { return $entries; } - $entries = $this->access->countUsers($filter); + $entries = $this->access->countUsers($filter, limit:$limit); $this->access->connection->writeToCache($cacheKey, $entries); return $entries; } @@ -619,7 +598,6 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I $uuid, true ); - $this->access->cacheUserExists($username); } else { $this->logger->warning( 'Failed to map created LDAP user with userid {userid}, because UUID could not be determined', @@ -630,10 +608,10 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I ); } } else { - throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean."); + throw new \UnexpectedValueException('LDAP Plugin: Method createUser changed to return the user DN instead of boolean.'); } } - return (bool) $dn; + return (bool)$dn; } return false; } diff --git a/apps/user_ldap/lib/User_Proxy.php b/apps/user_ldap/lib/User_Proxy.php index df9e025f871..0d41f495ce9 100644 --- a/apps/user_ldap/lib/User_Proxy.php +++ b/apps/user_ldap/lib/User_Proxy.php @@ -10,74 +10,38 @@ namespace OCA\User_LDAP; use OCA\User_LDAP\User\DeletedUsersIndex; use OCA\User_LDAP\User\OfflineUser; use OCA\User_LDAP\User\User; -use OCP\IConfig; use OCP\IUserBackend; -use OCP\IUserSession; use OCP\Notification\IManager as INotificationManager; use OCP\User\Backend\ICountMappedUsersBackend; -use OCP\User\Backend\ICountUsersBackend; +use OCP\User\Backend\ILimitAwareCountUsersBackend; use OCP\User\Backend\IProvideEnabledStateBackend; use OCP\UserInterface; use Psr\Log\LoggerInterface; -class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP, ICountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend { - /** @var User_LDAP[] */ - private array $backends = []; - private ?User_LDAP $refBackend = null; - - private bool $isSetUp = false; - private Helper $helper; - private IConfig $ocConfig; - private INotificationManager $notificationManager; - private IUserSession $userSession; - private UserPluginManager $userPluginManager; - private LoggerInterface $logger; - private DeletedUsersIndex $deletedUsersIndex; - +/** + * @template-extends Proxy<User_LDAP> + */ +class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend { public function __construct( - Helper $helper, + private Helper $helper, ILDAPWrapper $ldap, AccessFactory $accessFactory, - IConfig $ocConfig, - INotificationManager $notificationManager, - IUserSession $userSession, - UserPluginManager $userPluginManager, - LoggerInterface $logger, - DeletedUsersIndex $deletedUsersIndex, + private INotificationManager $notificationManager, + private UserPluginManager $userPluginManager, + private LoggerInterface $logger, + private DeletedUsersIndex $deletedUsersIndex, ) { - parent::__construct($ldap, $accessFactory); - $this->helper = $helper; - $this->ocConfig = $ocConfig; - $this->notificationManager = $notificationManager; - $this->userSession = $userSession; - $this->userPluginManager = $userPluginManager; - $this->logger = $logger; - $this->deletedUsersIndex = $deletedUsersIndex; + parent::__construct($helper, $ldap, $accessFactory); } - protected function setup(): void { - if ($this->isSetUp) { - return; - } - - $serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true); - foreach ($serverConfigPrefixes as $configPrefix) { - $this->backends[$configPrefix] = new User_LDAP( - $this->getAccess($configPrefix), - $this->ocConfig, - $this->notificationManager, - $this->userSession, - $this->userPluginManager, - $this->logger, - $this->deletedUsersIndex, - ); - - if (is_null($this->refBackend)) { - $this->refBackend = $this->backends[$configPrefix]; - } - } - - $this->isSetUp = true; + protected function newInstance(string $configPrefix): User_LDAP { + return new User_LDAP( + $this->getAccess($configPrefix), + $this->notificationManager, + $this->userPluginManager, + $this->logger, + $this->deletedUsersIndex, + ); } /** @@ -230,8 +194,8 @@ class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP /** * check if a user exists on LDAP * - * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user - * name or an instance of that user + * @param string|User $user either the Nextcloud user + * name or an instance of that user */ public function userExistsOnLDAP($user, bool $ignoreCache = false): bool { $id = ($user instanceof User) ? $user->getUsername() : $user; @@ -305,7 +269,7 @@ class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP } /** - * checks whether the user is allowed to change his avatar in Nextcloud + * 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 @@ -370,17 +334,21 @@ class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP /** * Count the number of users - * - * @return int|false */ - public function countUsers() { + public function countUsers(int $limit = 0): int|false { $this->setup(); $users = false; foreach ($this->backends as $backend) { - $backendUsers = $backend->countUsers(); + $backendUsers = $backend->countUsers($limit); if ($backendUsers !== false) { $users = (int)$users + $backendUsers; + if ($limit > 0) { + if ($users >= $limit) { + break; + } + $limit -= $users; + } } } return $users; @@ -447,11 +415,11 @@ class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP if ($search !== '') { $disabledUsers = array_filter( $disabledUsers, - fn (OfflineUser $user): bool => - mb_stripos($user->getOCName(), $search) !== false || - mb_stripos($user->getUID(), $search) !== false || - mb_stripos($user->getDisplayName(), $search) !== false || - mb_stripos($user->getEmail(), $search) !== false, + fn (OfflineUser $user): bool + => mb_stripos($user->getOCName(), $search) !== false + || mb_stripos($user->getUID(), $search) !== false + || mb_stripos($user->getDisplayName(), $search) !== false + || mb_stripos($user->getEmail(), $search) !== false, ); } return array_map( diff --git a/apps/user_ldap/lib/Wizard.php b/apps/user_ldap/lib/Wizard.php index 3720b509068..15a9f9cb212 100644 --- a/apps/user_ldap/lib/Wizard.php +++ b/apps/user_ldap/lib/Wizard.php @@ -11,13 +11,13 @@ namespace OCA\User_LDAP; use OC\ServerNotAvailableException; use OCP\IL10N; use OCP\L10N\IFactory as IL10NFactory; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; class Wizard extends LDAPUtility { protected static ?IL10N $l = null; - protected Access $access; protected ?\LDAP\Connection $cr = null; - protected Configuration $configuration; protected WizardResult $result; protected LoggerInterface $logger; @@ -35,18 +35,16 @@ class Wizard extends LDAPUtility { public const LDAP_NW_TIMEOUT = 4; public function __construct( - Configuration $configuration, + protected Configuration $configuration, ILDAPWrapper $ldap, - Access $access + protected Access $access, ) { parent::__construct($ldap); - $this->configuration = $configuration; if (is_null(static::$l)) { - static::$l = \OC::$server->get(IL10NFactory::class)->get('user_ldap'); + static::$l = Server::get(IL10NFactory::class)->get('user_ldap'); } - $this->access = $access; $this->result = new WizardResult(); - $this->logger = \OC::$server->get(LoggerInterface::class); + $this->logger = Server::get(LoggerInterface::class); } public function __destruct() { @@ -262,8 +260,8 @@ class Wizard extends LDAPUtility { $this->applyFind('ldap_email_attr', $winner); if ($writeLog) { $this->logger->info( - 'The mail attribute has automatically been reset, '. - 'because the original value did not return any results.', + 'The mail attribute has automatically been reset, ' + . 'because the original value did not return any results.', ['app' => 'user_ldap'] ); } @@ -402,7 +400,7 @@ class Wizard extends LDAPUtility { $filterParts = []; foreach ($obclasses as $obclass) { - $filterParts[] = 'objectclass='.$obclass; + $filterParts[] = 'objectclass=' . $obclass; } //we filter for everything //- that looks like a group and @@ -647,7 +645,7 @@ class Wizard extends LDAPUtility { $p = $setting['port']; $t = $setting['tls']; $this->logger->debug( - 'Wiz: trying port '. $p . ', TLS '. $t, + 'Wiz: trying port ' . $p . ', TLS ' . $t, ['app' => 'user_ldap'] ); //connectAndBind may throw Exception, it needs to be caught by the @@ -712,7 +710,7 @@ class Wizard extends LDAPUtility { //this did not help :( //Let's see whether we can parse the Host URL and convert the domain to //a base DN - $helper = \OC::$server->get(Helper::class); + $helper = Server::get(Helper::class); $domain = $helper->getDomainFromURL($this->configuration->ldapHost); if (!$domain) { return false; @@ -755,7 +753,7 @@ class Wizard extends LDAPUtility { //removes Port from Host if (is_array($hostInfo) && isset($hostInfo['port'])) { $port = $hostInfo['port']; - $host = str_replace(':'.$port, '', $host); + $host = str_replace(':' . $port, '', $host); $this->applyFind('ldap_host', $host); $this->applyFind('ldap_port', (string)$port); } @@ -824,7 +822,7 @@ class Wizard extends LDAPUtility { $errorNo = $this->ldap->errno($cr); $errorMsg = $this->ldap->error($cr); $this->logger->info( - 'Wiz: Could not search base '.$base.' Error '.$errorNo.': '.$errorMsg, + 'Wiz: Could not search base ' . $base . ' Error ' . $errorNo . ': ' . $errorMsg, ['app' => 'user_ldap'] ); return false; @@ -858,8 +856,8 @@ class Wizard extends LDAPUtility { /** * creates an LDAP Filter from given configuration * @param int $filterType int, for which use case the filter shall be created - * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or - * self::LFILTER_GROUP_LIST + * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or + * self::LFILTER_GROUP_LIST * @throws \Exception */ private function composeLdapFilter(int $filterType): string { @@ -902,7 +900,7 @@ class Wizard extends LDAPUtility { $filterPart = '(memberof=' . ldap_escape($dn, '', LDAP_ESCAPE_FILTER) . ')'; if (isset($attrs['primaryGroupToken'])) { $pgt = $attrs['primaryGroupToken'][0]; - $primaryFilterPart = '(primaryGroupID=' . ldap_escape($pgt, '', LDAP_ESCAPE_FILTER) .')'; + $primaryFilterPart = '(primaryGroupID=' . ldap_escape($pgt, '', LDAP_ESCAPE_FILTER) . ')'; $filterPart = '(|' . $filterPart . $primaryFilterPart . ')'; } $filter .= $filterPart; @@ -1002,12 +1000,12 @@ class Wizard extends LDAPUtility { $filterLogin .= ')'; } - $filter = '(&'.$ulf.$filterLogin.')'; + $filter = '(&' . $ulf . $filterLogin . ')'; break; } $this->logger->debug( - 'Wiz: Final filter '.$filter, + 'Wiz: Final filter ' . $filter, ['app' => 'user_ldap'] ); @@ -1069,7 +1067,7 @@ class Wizard extends LDAPUtility { if ($login === true) { $this->logger->debug( - 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, + 'Wiz: Bind successful to Port ' . $port . ' TLS ' . (int)$tls, ['app' => 'user_ldap'] ); return true; @@ -1114,9 +1112,9 @@ class Wizard extends LDAPUtility { * @param string[] $filters array, the filters that shall be used in the search * @param string $attr the attribute of which a list of values shall be returned * @param int $dnReadLimit the amount of how many DNs should be analyzed. - * The lower, the faster + * The lower, the faster * @param string $maxF string. if not null, this variable will have the filter that - * yields most result entries + * yields most result entries * @return array|false an array with the values on success, false otherwise */ public function cumulativeSearchOnAttribute(array $filters, string $attr, int $dnReadLimit = 3, ?string &$maxF = null) { @@ -1190,9 +1188,9 @@ class Wizard extends LDAPUtility { * @param string $attr the attribute to look for * @param string $dbkey the dbkey of the setting the feature is connected to * @param string $confkey the confkey counterpart for the $dbkey as used in the - * Configuration class + * Configuration class * @param bool $po whether the objectClass with most result entries - * shall be pre-selected via the result + * shall be pre-selected via the result * @return array list of found items. * @throws \Exception */ @@ -1203,7 +1201,7 @@ class Wizard extends LDAPUtility { } $p = 'objectclass='; foreach ($objectclasses as $key => $value) { - $objectclasses[$key] = $p.$value; + $objectclasses[$key] = $p . $value; } $maxEntryObjC = ''; @@ -1211,8 +1209,8 @@ class Wizard extends LDAPUtility { //When looking for objectclasses, testing few entries is sufficient, $dig = 3; - $availableFeatures = - $this->cumulativeSearchOnAttribute($objectclasses, $attr, + $availableFeatures + = $this->cumulativeSearchOnAttribute($objectclasses, $attr, $dig, $maxEntryObjC); if (is_array($availableFeatures) && count($availableFeatures) > 0) { @@ -1244,7 +1242,7 @@ class Wizard extends LDAPUtility { * @param string $attribute the attribute values to look for * @param array &$known new values will be appended here * @return int state on of the class constants LRESULT_PROCESSED_OK, - * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP + * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP */ private function getAttributeValuesFromEntry(array $result, string $attribute, array &$known): int { if (!isset($result['count']) @@ -1253,7 +1251,7 @@ class Wizard extends LDAPUtility { } // strtolower on all keys for proper comparison - $result = \OCP\Util::mb_array_change_key_case($result); + $result = Util::mb_array_change_key_case($result); $attribute = strtolower($attribute); if (isset($result[$attribute])) { foreach ($result[$attribute] as $key => $val) { diff --git a/apps/user_ldap/lib/WizardResult.php b/apps/user_ldap/lib/WizardResult.php index 09aadd2b781..d6fd67d4204 100644 --- a/apps/user_ldap/lib/WizardResult.php +++ b/apps/user_ldap/lib/WizardResult.php @@ -20,7 +20,7 @@ class WizardResult { $this->changes[$key] = $value; } - + public function markChange() { $this->markedChange = true; } |