From 474b5e1a058febc18ddf5222f86c741cf09ba29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 6 Jan 2022 16:33:18 +0100 Subject: [PATCH 1/7] Add FirstLoginListener to accept shares upon first ldap user login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/user_ldap/lib/AppInfo/Application.php | 13 ++ apps/user_ldap/lib/FirstLoginListener.php | 140 +++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 apps/user_ldap/lib/FirstLoginListener.php diff --git a/apps/user_ldap/lib/AppInfo/Application.php b/apps/user_ldap/lib/AppInfo/Application.php index d6fb062a0285a..a497dfecb55db 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -32,6 +32,7 @@ use OCA\User_LDAP\Events\GroupBackendRegistered; use OCA\User_LDAP\Events\UserBackendRegistered; use OCA\User_LDAP\FilesystemHelper; +use OCA\User_LDAP\FirstLoginListener; use OCA\User_LDAP\Group_Proxy; use OCA\User_LDAP\GroupPluginManager; use OCA\User_LDAP\Handler\ExtStorageConfigHandler; @@ -57,6 +58,7 @@ use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; use OCP\Share\IManager as IShareManager; +use OCP\User\Events\PostLoginEvent; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -142,6 +144,7 @@ public function boot(IBootContext $context): void { }); $context->injectFn(Closure::fromCallable([$this, 'registerBackendDependents'])); + $context->injectFn(Closure::fromCallable([$this, 'registerFirstLoginListener'])); \OCP\Util::connectHook( '\OCA\Files_Sharing\API\Server2Server', @@ -162,4 +165,14 @@ function () use ($appContainer) { } ); } + + private function registerFirstLoginListener(IEventDispatcher $dispatcher) { + $dispatcher->addServiceListener(PostLoginEvent::class, FirstLoginListener::class); + \OCP\Util::connectHook( + '\OC\User', + 'assignedUserId', + FirstLoginListener::class, + 'onAssignedId' + ); + } } diff --git a/apps/user_ldap/lib/FirstLoginListener.php b/apps/user_ldap/lib/FirstLoginListener.php new file mode 100644 index 0000000000000..2bfe4a46c16db --- /dev/null +++ b/apps/user_ldap/lib/FirstLoginListener.php @@ -0,0 +1,140 @@ + + * + * @author Côme Chilliet + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\User_LDAP; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\UserAddedEvent; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\PostLoginEvent; +use Psr\Log\LoggerInterface; + +class FirstLoginListener implements IEventListener { + protected $somekindofstatefulhandler; + + /** @var Group_Proxy */ + private $groupBackend; + /** @var IEventDispatcher */ + private $dispatcher; + /** @var IGroupManager */ + private $groupManager; + /** @var IUserManager */ + private $userManager; + /** @var LoggerInterface */ + private $logger; + /** @var IDBConnection */ + private $dbc; + + public function __construct( + Group_Proxy $groupBackend, + IEventDispatcher $dispatcher, + IGroupManager $groupManager, + IUserManager $userManager, + LoggerInterface $logger, + IDBConnection $dbc + ) { + $this->groupBackend = $groupBackend; + $this->dispatcher = $dispatcher; + $this->groupManager = $groupManager; + $this->userManager = $userManager; + $this->logger = $logger; + $this->dbc = $dbc; + } + + public function handle(Event $event): void { + if ($event instanceof PostLoginEvent) { + $this->onPostLogin($event->getUser()->getUID()); + } + } + + public function onAssignedId(string $username): void { + $this->somekindofstatefulhandler[$username]['id'] = 1; + $this->triggerUpdateGroups($username); + } + + public function onPostLogin(string $username): void { + $this->somekindofstatefulhandler[$username]['login'] = 1; + $this->triggerUpdateGroups($username); + } + + private function triggerUpdateGroups(string $username): void { + if (array_sum($this->somekindofstatefulhandler[$username] ?? []) >= 2) { + $this->updateGroups($username); + } + } + + private function updateGroups(string $username): void { + $groups = $this->groupBackend->getUserGroups($username); + + $qb = $this->dbc->getQueryBuilder(); + $qb->select(['owncloudusers']) + ->from('ldap_group_members') + ->where($qb->expr()->eq('owncloudname', $qb->createParameter('groupId'))); + + $qbUpdate = $this->dbc->getQueryBuilder(); + $qbUpdate->update('ldap_group_members') + ->set('owncloudusers', $qb->createParameter('members')) + ->where($qb->expr()->eq('owncloudname', $qb->createParameter('groupId'))); + + foreach ($groups as $group) { + $qb->setParameters([ + 'groupId' => $group + ]); + + $qResult = $qb->execute(); + $data = $qResult->fetchOne(); + $qResult->closeCursor(); + + $knownUsers = unserialize($data['owncloudusers']); + $hasChanged = false; + + $groupObject = $this->groupManager->get($group); + if (!in_array($username, $knownUsers)) { + $userObject = $this->userManager->get($username); + if ($userObject instanceof IUser) { + $this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject)); + $this->logger->info( + __CLASS__ . ' – {user} added to {group}', + [ + 'app' => 'user_ldap', + 'user' => $username, + 'group' => $group + ] + ); + $qbUpdate->setParameters([ + 'members' => serialize(array_merge($knownUsers, [$username])), + 'groupId' => $group + ]); + $qbUpdate->execute(); + } + } + } + } +} From 2e52e39aff4426cd62c02b8c2262e39e51fd622e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 10 Feb 2022 13:11:50 +0100 Subject: [PATCH 2/7] Use a standard array for the stateful cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/user_ldap/lib/FirstLoginListener.php | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/user_ldap/lib/FirstLoginListener.php b/apps/user_ldap/lib/FirstLoginListener.php index 2bfe4a46c16db..997573121ca81 100644 --- a/apps/user_ldap/lib/FirstLoginListener.php +++ b/apps/user_ldap/lib/FirstLoginListener.php @@ -37,7 +37,8 @@ use Psr\Log\LoggerInterface; class FirstLoginListener implements IEventListener { - protected $somekindofstatefulhandler; + /** @var array> */ + private $eventHappened = []; /** @var Group_Proxy */ private $groupBackend; @@ -75,17 +76,17 @@ public function handle(Event $event): void { } public function onAssignedId(string $username): void { - $this->somekindofstatefulhandler[$username]['id'] = 1; + $this->eventHappened[$username]['id'] = 1; $this->triggerUpdateGroups($username); } public function onPostLogin(string $username): void { - $this->somekindofstatefulhandler[$username]['login'] = 1; + $this->eventHappened[$username]['login'] = 1; $this->triggerUpdateGroups($username); } private function triggerUpdateGroups(string $username): void { - if (array_sum($this->somekindofstatefulhandler[$username] ?? []) >= 2) { + if (array_sum($this->eventHappened[$username] ?? []) >= 2) { $this->updateGroups($username); } } @@ -108,7 +109,7 @@ private function updateGroups(string $username): void { 'groupId' => $group ]); - $qResult = $qb->execute(); + $qResult = $qb->executeQuery(); $data = $qResult->fetchOne(); $qResult->closeCursor(); @@ -116,6 +117,17 @@ private function updateGroups(string $username): void { $hasChanged = false; $groupObject = $this->groupManager->get($group); + if ($groupObject === null) { + $this->logger->error( + __CLASS__ . ' – group {group} could not be found (user {user})', + [ + 'app' => 'user_ldap', + 'user' => $username, + 'group' => $group + ] + ); + continue; + } if (!in_array($username, $knownUsers)) { $userObject = $this->userManager->get($username); if ($userObject instanceof IUser) { @@ -132,7 +144,7 @@ private function updateGroups(string $username): void { 'members' => serialize(array_merge($knownUsers, [$username])), 'groupId' => $group ]); - $qbUpdate->execute(); + $qbUpdate->executeStatement(); } } } From 4f2048851cbfdc2bd057f2f79824c58e128be2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Thu, 10 Feb 2022 14:43:48 +0100 Subject: [PATCH 3/7] Move event listener registration to register() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/user_ldap/lib/AppInfo/Application.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/user_ldap/lib/AppInfo/Application.php b/apps/user_ldap/lib/AppInfo/Application.php index a497dfecb55db..bcedb80d604cc 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -115,6 +115,7 @@ function (ContainerInterface $c) { // the instance is specific to a lazy bound Access instance, thus cannot be shared. false ); + $context->registerEventListener(PostLoginEvent::class, FirstLoginListener::class); } public function boot(IBootContext $context): void { @@ -144,7 +145,6 @@ public function boot(IBootContext $context): void { }); $context->injectFn(Closure::fromCallable([$this, 'registerBackendDependents'])); - $context->injectFn(Closure::fromCallable([$this, 'registerFirstLoginListener'])); \OCP\Util::connectHook( '\OCA\Files_Sharing\API\Server2Server', @@ -152,9 +152,15 @@ public function boot(IBootContext $context): void { '\OCA\User_LDAP\Helper', 'loginName2UserName' ); + \OCP\Util::connectHook( + '\OC\User', + 'assignedUserId', + FirstLoginListener::class, + 'onAssignedId' + ); } - private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher) { + private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher): void { $dispatcher->addListener( 'OCA\\Files_External::loadAdditionalBackends', function () use ($appContainer) { @@ -165,14 +171,4 @@ function () use ($appContainer) { } ); } - - private function registerFirstLoginListener(IEventDispatcher $dispatcher) { - $dispatcher->addServiceListener(PostLoginEvent::class, FirstLoginListener::class); - \OCP\Util::connectHook( - '\OC\User', - 'assignedUserId', - FirstLoginListener::class, - 'onAssignedId' - ); - } } From 6e38cb197a19e810c7ff17c0f29147786998af11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 14 Feb 2022 12:35:01 +0100 Subject: [PATCH 4/7] Add logging to be able to debug FirstLoginListener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/user_ldap/lib/FirstLoginListener.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/user_ldap/lib/FirstLoginListener.php b/apps/user_ldap/lib/FirstLoginListener.php index 997573121ca81..357cc42fc6df0 100644 --- a/apps/user_ldap/lib/FirstLoginListener.php +++ b/apps/user_ldap/lib/FirstLoginListener.php @@ -76,11 +76,25 @@ public function handle(Event $event): void { } public function onAssignedId(string $username): void { + $this->logger->info( + __CLASS__ . ' – {user} assignedId', + [ + 'app' => 'user_ldap', + 'user' => $username, + ] + ); $this->eventHappened[$username]['id'] = 1; $this->triggerUpdateGroups($username); } public function onPostLogin(string $username): void { + $this->logger->info( + __CLASS__ . ' – {user} postLogin', + [ + 'app' => 'user_ldap', + 'user' => $username, + ] + ); $this->eventHappened[$username]['login'] = 1; $this->triggerUpdateGroups($username); } @@ -92,6 +106,13 @@ private function triggerUpdateGroups(string $username): void { } private function updateGroups(string $username): void { + $this->logger->info( + __CLASS__ . ' – {user} updateGroups', + [ + 'app' => 'user_ldap', + 'user' => $username, + ] + ); $groups = $this->groupBackend->getUserGroups($username); $qb = $this->dbc->getQueryBuilder(); From 800b1b703195e494d980b741ee566ba2b19d9b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 14 Feb 2022 12:35:28 +0100 Subject: [PATCH 5/7] Fire group events at login for LDAP groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../user_ldap/composer/composer/installed.php | 4 +- apps/user_ldap/lib/AppInfo/Application.php | 10 +- .../lib/Db/GroupMembershipMapper.php | 12 ++ apps/user_ldap/lib/FirstLoginListener.php | 173 ------------------ apps/user_ldap/lib/LoginListener.php | 131 +++++++++++++ 7 files changed, 149 insertions(+), 183 deletions(-) delete mode 100644 apps/user_ldap/lib/FirstLoginListener.php create mode 100644 apps/user_ldap/lib/LoginListener.php diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index 74ffce2caff94..6f7e5a8c1dad1 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -55,6 +55,7 @@ 'OCA\\User_LDAP\\LDAPProvider' => $baseDir . '/../lib/LDAPProvider.php', 'OCA\\User_LDAP\\LDAPProviderFactory' => $baseDir . '/../lib/LDAPProviderFactory.php', 'OCA\\User_LDAP\\LDAPUtility' => $baseDir . '/../lib/LDAPUtility.php', + 'OCA\\User_LDAP\\LoginListener' => $baseDir . '/../lib/LoginListener.php', 'OCA\\User_LDAP\\Mapping\\AbstractMapping' => $baseDir . '/../lib/Mapping/AbstractMapping.php', 'OCA\\User_LDAP\\Mapping\\GroupMapping' => $baseDir . '/../lib/Mapping/GroupMapping.php', 'OCA\\User_LDAP\\Mapping\\UserMapping' => $baseDir . '/../lib/Mapping/UserMapping.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index cead1740b88ee..9932166b96026 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -70,6 +70,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\LDAPProvider' => __DIR__ . '/..' . '/../lib/LDAPProvider.php', 'OCA\\User_LDAP\\LDAPProviderFactory' => __DIR__ . '/..' . '/../lib/LDAPProviderFactory.php', 'OCA\\User_LDAP\\LDAPUtility' => __DIR__ . '/..' . '/../lib/LDAPUtility.php', + 'OCA\\User_LDAP\\LoginListener' => __DIR__ . '/..' . '/../lib/LoginListener.php', 'OCA\\User_LDAP\\Mapping\\AbstractMapping' => __DIR__ . '/..' . '/../lib/Mapping/AbstractMapping.php', 'OCA\\User_LDAP\\Mapping\\GroupMapping' => __DIR__ . '/..' . '/../lib/Mapping/GroupMapping.php', 'OCA\\User_LDAP\\Mapping\\UserMapping' => __DIR__ . '/..' . '/../lib/Mapping/UserMapping.php', diff --git a/apps/user_ldap/composer/composer/installed.php b/apps/user_ldap/composer/composer/installed.php index 82b21976e1b24..34d21903bce1d 100644 --- a/apps/user_ldap/composer/composer/installed.php +++ b/apps/user_ldap/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0ecd81bfdcfcd878556de3485d292fb4ea340d9e', + 'reference' => '722b062d3fb372799000591b8d23d3b65a4e50db', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0ecd81bfdcfcd878556de3485d292fb4ea340d9e', + 'reference' => '722b062d3fb372799000591b8d23d3b65a4e50db', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/user_ldap/lib/AppInfo/Application.php b/apps/user_ldap/lib/AppInfo/Application.php index bcedb80d604cc..1b6c8cab0fd3c 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -32,13 +32,13 @@ use OCA\User_LDAP\Events\GroupBackendRegistered; use OCA\User_LDAP\Events\UserBackendRegistered; use OCA\User_LDAP\FilesystemHelper; -use OCA\User_LDAP\FirstLoginListener; use OCA\User_LDAP\Group_Proxy; use OCA\User_LDAP\GroupPluginManager; use OCA\User_LDAP\Handler\ExtStorageConfigHandler; use OCA\User_LDAP\Helper; use OCA\User_LDAP\ILDAPWrapper; use OCA\User_LDAP\LDAP; +use OCA\User_LDAP\LoginListener; use OCA\User_LDAP\Notification\Notifier; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User_Proxy; @@ -115,7 +115,7 @@ function (ContainerInterface $c) { // the instance is specific to a lazy bound Access instance, thus cannot be shared. false ); - $context->registerEventListener(PostLoginEvent::class, FirstLoginListener::class); + $context->registerEventListener(PostLoginEvent::class, LoginListener::class); } public function boot(IBootContext $context): void { @@ -152,12 +152,6 @@ public function boot(IBootContext $context): void { '\OCA\User_LDAP\Helper', 'loginName2UserName' ); - \OCP\Util::connectHook( - '\OC\User', - 'assignedUserId', - FirstLoginListener::class, - 'onAssignedId' - ); } private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher): void { diff --git a/apps/user_ldap/lib/Db/GroupMembershipMapper.php b/apps/user_ldap/lib/Db/GroupMembershipMapper.php index 8f6af16b267e6..cd722b64f044c 100644 --- a/apps/user_ldap/lib/Db/GroupMembershipMapper.php +++ b/apps/user_ldap/lib/Db/GroupMembershipMapper.php @@ -64,6 +64,18 @@ public function findGroupMemberships(string $groupid): array { return $this->findEntities($select); } + /** + * @return GroupMembership[] + */ + public function findGroupMembershipsForUser(string $userid): array { + $qb = $this->db->getQueryBuilder(); + $select = $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userid))); + + return $this->findEntities($select); + } + public function deleteGroups(array $removedGroups): void { $query = $this->db->getQueryBuilder(); $query->delete($this->getTableName()) diff --git a/apps/user_ldap/lib/FirstLoginListener.php b/apps/user_ldap/lib/FirstLoginListener.php deleted file mode 100644 index 357cc42fc6df0..0000000000000 --- a/apps/user_ldap/lib/FirstLoginListener.php +++ /dev/null @@ -1,173 +0,0 @@ - - * - * @author Côme Chilliet - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ -namespace OCA\User_LDAP; - -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\EventDispatcher\IEventListener; -use OCP\Group\Events\UserAddedEvent; -use OCP\IDBConnection; -use OCP\IGroupManager; -use OCP\IUser; -use OCP\IUserManager; -use OCP\User\Events\PostLoginEvent; -use Psr\Log\LoggerInterface; - -class FirstLoginListener implements IEventListener { - /** @var array> */ - private $eventHappened = []; - - /** @var Group_Proxy */ - private $groupBackend; - /** @var IEventDispatcher */ - private $dispatcher; - /** @var IGroupManager */ - private $groupManager; - /** @var IUserManager */ - private $userManager; - /** @var LoggerInterface */ - private $logger; - /** @var IDBConnection */ - private $dbc; - - public function __construct( - Group_Proxy $groupBackend, - IEventDispatcher $dispatcher, - IGroupManager $groupManager, - IUserManager $userManager, - LoggerInterface $logger, - IDBConnection $dbc - ) { - $this->groupBackend = $groupBackend; - $this->dispatcher = $dispatcher; - $this->groupManager = $groupManager; - $this->userManager = $userManager; - $this->logger = $logger; - $this->dbc = $dbc; - } - - public function handle(Event $event): void { - if ($event instanceof PostLoginEvent) { - $this->onPostLogin($event->getUser()->getUID()); - } - } - - public function onAssignedId(string $username): void { - $this->logger->info( - __CLASS__ . ' – {user} assignedId', - [ - 'app' => 'user_ldap', - 'user' => $username, - ] - ); - $this->eventHappened[$username]['id'] = 1; - $this->triggerUpdateGroups($username); - } - - public function onPostLogin(string $username): void { - $this->logger->info( - __CLASS__ . ' – {user} postLogin', - [ - 'app' => 'user_ldap', - 'user' => $username, - ] - ); - $this->eventHappened[$username]['login'] = 1; - $this->triggerUpdateGroups($username); - } - - private function triggerUpdateGroups(string $username): void { - if (array_sum($this->eventHappened[$username] ?? []) >= 2) { - $this->updateGroups($username); - } - } - - private function updateGroups(string $username): void { - $this->logger->info( - __CLASS__ . ' – {user} updateGroups', - [ - 'app' => 'user_ldap', - 'user' => $username, - ] - ); - $groups = $this->groupBackend->getUserGroups($username); - - $qb = $this->dbc->getQueryBuilder(); - $qb->select(['owncloudusers']) - ->from('ldap_group_members') - ->where($qb->expr()->eq('owncloudname', $qb->createParameter('groupId'))); - - $qbUpdate = $this->dbc->getQueryBuilder(); - $qbUpdate->update('ldap_group_members') - ->set('owncloudusers', $qb->createParameter('members')) - ->where($qb->expr()->eq('owncloudname', $qb->createParameter('groupId'))); - - foreach ($groups as $group) { - $qb->setParameters([ - 'groupId' => $group - ]); - - $qResult = $qb->executeQuery(); - $data = $qResult->fetchOne(); - $qResult->closeCursor(); - - $knownUsers = unserialize($data['owncloudusers']); - $hasChanged = false; - - $groupObject = $this->groupManager->get($group); - if ($groupObject === null) { - $this->logger->error( - __CLASS__ . ' – group {group} could not be found (user {user})', - [ - 'app' => 'user_ldap', - 'user' => $username, - 'group' => $group - ] - ); - continue; - } - if (!in_array($username, $knownUsers)) { - $userObject = $this->userManager->get($username); - if ($userObject instanceof IUser) { - $this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject)); - $this->logger->info( - __CLASS__ . ' – {user} added to {group}', - [ - 'app' => 'user_ldap', - 'user' => $username, - 'group' => $group - ] - ); - $qbUpdate->setParameters([ - 'members' => serialize(array_merge($knownUsers, [$username])), - 'groupId' => $group - ]); - $qbUpdate->executeStatement(); - } - } - } - } -} diff --git a/apps/user_ldap/lib/LoginListener.php b/apps/user_ldap/lib/LoginListener.php new file mode 100644 index 0000000000000..b9c3c3c17428c --- /dev/null +++ b/apps/user_ldap/lib/LoginListener.php @@ -0,0 +1,131 @@ + + * + * @author Côme Chilliet + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\User_LDAP; + +use OCA\User_LDAP\Db\GroupMembership; +use OCA\User_LDAP\Db\GroupMembershipMapper; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\User\Events\PostLoginEvent; +use Psr\Log\LoggerInterface; + +/** + * @template-implements IEventListener + */ +class LoginListener implements IEventListener { + public function __construct( + private IEventDispatcher $dispatcher, + private Group_Proxy $groupBackend, + private IGroupManager $groupManager, + private LoggerInterface $logger, + private GroupMembershipMapper $groupMembershipMapper, + ) { + } + + public function handle(Event $event): void { + if ($event instanceof PostLoginEvent) { + $this->onPostLogin($event->getUser()); + } + } + + public function onPostLogin(IUser $user): void { + $this->logger->info( + __CLASS__ . ' – {user} postLogin', + [ + 'app' => 'user_ldap', + 'user' => $user->getUID(), + ] + ); + $this->updateGroups($user); + } + + private function updateGroups(IUser $userObject): void { + $userId = $userObject->getUID(); + $groupMemberships = $this->groupMembershipMapper->findGroupMembershipsForUser($userId); + $knownGroups = array_map( + static fn (GroupMembership $groupMembership): string => $groupMembership->getGroupid(), + $groupMemberships + ); + $groupMemberships = array_combine($knownGroups, $groupMemberships); + $actualGroups = $this->groupBackend->getUserGroups($userId); + + $newGroups = array_diff($actualGroups, $knownGroups); + $oldGroups = array_diff($knownGroups, $actualGroups); + foreach ($newGroups as $groupId) { + $groupObject = $this->groupManager->get($groupId); + if ($groupObject === null) { + $this->logger->error( + __CLASS__ . ' – group {group} could not be found (user {user})', + [ + 'app' => 'user_ldap', + 'user' => $userId, + 'group' => $groupId + ] + ); + continue; + } + $this->groupMembershipMapper->insert(GroupMembership::fromParams(['groupid' => $groupId,'userid' => $userId])); + // TODO: empty cache to avoid crash + $this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject)); + $this->logger->info( + __CLASS__ . ' – {user} added to {group}', + [ + 'app' => 'user_ldap', + 'user' => $userId, + 'group' => $groupId + ] + ); + } + foreach ($oldGroups as $groupId) { + $this->groupMembershipMapper->delete($groupMemberships[$groupId]); + $groupObject = $this->groupManager->get($groupId); + if ($groupObject === null) { + $this->logger->error( + __CLASS__ . ' – group {group} could not be found (user {user})', + [ + 'app' => 'user_ldap', + 'user' => $userId, + 'group' => $groupId + ] + ); + continue; + } + $this->dispatcher->dispatchTyped(new UserRemovedEvent($groupObject, $userObject)); + $this->logger->info( + 'service "updateGroups" – {user} removed from {group}', + [ + 'user' => $userId, + 'group' => $groupId + ] + ); + } + } +} From 05efbf11d95109c7905a554b92ee7924cc6115e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 12 Sep 2023 12:15:30 +0200 Subject: [PATCH 6/7] Fix LDAP LoginListener by adding new group relationships to caches before firing the event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/user_ldap/lib/Group_LDAP.php | 35 ++++++++++++++++++++++++++-- apps/user_ldap/lib/Group_Proxy.php | 4 ++++ apps/user_ldap/lib/LoginListener.php | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index b3ff63d3b5c0f..544c7f151ac63 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -56,9 +56,9 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend { protected bool $enabled = false; - /** @var CappedMemoryCache $cachedGroupMembers array of users with gid as key */ + /** @var CappedMemoryCache $cachedGroupMembers array of user DN with gid as key */ protected CappedMemoryCache $cachedGroupMembers; - /** @var CappedMemoryCache $cachedGroupsByMember array of groups with uid as key */ + /** @var CappedMemoryCache $cachedGroupsByMember array of groups with user DN as key */ protected CappedMemoryCache $cachedGroupsByMember; /** @var CappedMemoryCache $cachedNestedGroups array of groups with gid (DN) as key */ protected CappedMemoryCache $cachedNestedGroups; @@ -1357,4 +1357,35 @@ public function getDisplayName(string $gid): string { public function dn2GroupName(string $dn): string|false { return $this->access->dn2groupname($dn); } + + public function addRelationshipToCaches(string $uid, ?string $dnUser, string $gid): void { + $dnGroup = $this->access->groupname2dn($gid); + $dnUser ??= $this->access->username2dn($uid); + if ($dnUser === false || $dnGroup === false) { + return; + } + if (isset($this->cachedGroupMembers[$gid])) { + $this->cachedGroupMembers[$gid] = array_merge($this->cachedGroupMembers[$gid], [$dnUser]); + } + unset($this->cachedGroupsByMember[$dnUser]); + unset($this->cachedNestedGroups[$gid]); + $cacheKey = 'inGroup' . $uid . ':' . $gid; + $this->access->connection->writeToCache($cacheKey, true); + $cacheKeyMembers = 'inGroup-members:' . $gid; + if (!is_null($data = $this->access->connection->getFromCache($cacheKeyMembers))) { + $this->access->connection->writeToCache($cacheKeyMembers, array_merge($data, [$dnUser])); + } + $cacheKey = '_groupMembers' . $dnGroup; + if (!is_null($data = $this->access->connection->getFromCache($cacheKey))) { + $this->access->connection->writeToCache($cacheKey, array_merge($data, [$dnUser])); + } + $cacheKey = 'getUserGroups' . $uid; + if (!is_null($data = $this->access->connection->getFromCache($cacheKey))) { + $this->access->connection->writeToCache($cacheKey, array_merge($data, [$gid])); + } + // These cache keys cannot be easily updated: + // $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset; + // $cacheKey = 'usersInGroup-' . $gid . '-' . $search; + // $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search; + } } diff --git a/apps/user_ldap/lib/Group_Proxy.php b/apps/user_ldap/lib/Group_Proxy.php index 114902ff9bad7..bb3941565723b 100644 --- a/apps/user_ldap/lib/Group_Proxy.php +++ b/apps/user_ldap/lib/Group_Proxy.php @@ -384,4 +384,8 @@ public function getBackendName(): string { public function searchInGroup(string $gid, string $search = '', int $limit = -1, int $offset = 0): array { return $this->handleRequest($gid, 'searchInGroup', [$gid, $search, $limit, $offset]); } + + public function addRelationshipToCaches(string $uid, ?string $dnUser, string $gid): void { + $this->handleRequest($gid, 'addRelationshipToCaches', [$uid, $dnUser, $gid]); + } } diff --git a/apps/user_ldap/lib/LoginListener.php b/apps/user_ldap/lib/LoginListener.php index b9c3c3c17428c..ac5b32635c8f5 100644 --- a/apps/user_ldap/lib/LoginListener.php +++ b/apps/user_ldap/lib/LoginListener.php @@ -93,7 +93,7 @@ private function updateGroups(IUser $userObject): void { continue; } $this->groupMembershipMapper->insert(GroupMembership::fromParams(['groupid' => $groupId,'userid' => $userId])); - // TODO: empty cache to avoid crash + $this->groupBackend->addRelationshipToCaches($userId, null, $groupId); $this->dispatcher->dispatchTyped(new UserAddedEvent($groupObject, $userObject)); $this->logger->info( __CLASS__ . ' – {user} added to {group}', From 500374a8e7528b81e882d43c06b625839208474d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 12 Sep 2023 16:03:40 +0200 Subject: [PATCH 7/7] Fix registerEventListener signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It seems now psalm correctly supports this. Signed-off-by: Côme Chilliet --- lib/public/AppFramework/Bootstrap/IRegistrationContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 720803a78d170..c34cec38eb127 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -129,7 +129,7 @@ public function registerParameter(string $name, $value): void; * @param string $event preferably the fully-qualified class name of the Event sub class to listen for * @psalm-param string|class-string $event preferably the fully-qualified class name of the Event sub class to listen for * @param string $listener fully qualified class name (or ::class notation) of a \OCP\EventDispatcher\IEventListener that can be built by the DI container - * @psalm-param class-string<\OCP\EventDispatcher\IEventListener> $listener fully qualified class name that can be built by the DI container + * @psalm-param class-string<\OCP\EventDispatcher\IEventListener> $listener fully qualified class name that can be built by the DI container * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) *