diff options
author | Côme Chilliet <91878298+come-nc@users.noreply.github.com> | 2023-10-16 10:01:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-16 10:01:55 +0200 |
commit | 8212feefb9cf5796e6a92b27956092a1b5e933fe (patch) | |
tree | 8e38323a4e370fecfe22834b68cc7bb44dc289ea | |
parent | 356c2219bc935526ee0f304e28695552e4f0827e (diff) | |
parent | 500374a8e7528b81e882d43c06b625839208474d (diff) | |
download | nextcloud-server-8212feefb9cf5796e6a92b27956092a1b5e933fe.tar.gz nextcloud-server-8212feefb9cf5796e6a92b27956092a1b5e933fe.zip |
Merge pull request #40367 from nextcloud/fix/user_ldap-update-groups-on-login
Fire group membership events from LDAP at login
-rw-r--r-- | apps/user_ldap/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/user_ldap/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/user_ldap/composer/composer/installed.php | 4 | ||||
-rw-r--r-- | apps/user_ldap/lib/AppInfo/Application.php | 5 | ||||
-rw-r--r-- | apps/user_ldap/lib/Db/GroupMembershipMapper.php | 12 | ||||
-rw-r--r-- | apps/user_ldap/lib/Group_LDAP.php | 35 | ||||
-rw-r--r-- | apps/user_ldap/lib/Group_Proxy.php | 4 | ||||
-rw-r--r-- | apps/user_ldap/lib/LoginListener.php | 131 | ||||
-rw-r--r-- | lib/public/AppFramework/Bootstrap/IRegistrationContext.php | 2 |
9 files changed, 189 insertions, 6 deletions
diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index 74ffce2caff..6f7e5a8c1da 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -55,6 +55,7 @@ return array( '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 cead1740b88..9932166b960 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 82b21976e1b..34d21903bce 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 d6fb062a028..1b6c8cab0fd 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -38,6 +38,7 @@ 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; @@ -57,6 +58,7 @@ use OCP\IServerContainer; 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; @@ -113,6 +115,7 @@ class Application extends App implements IBootstrap { // the instance is specific to a lazy bound Access instance, thus cannot be shared. false ); + $context->registerEventListener(PostLoginEvent::class, LoginListener::class); } public function boot(IBootContext $context): void { @@ -151,7 +154,7 @@ class Application extends App implements IBootstrap { ); } - private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher) { + private function registerBackendDependents(IAppContainer $appContainer, IEventDispatcher $dispatcher): void { $dispatcher->addListener( 'OCA\\Files_External::loadAdditionalBackends', function () use ($appContainer) { diff --git a/apps/user_ldap/lib/Db/GroupMembershipMapper.php b/apps/user_ldap/lib/Db/GroupMembershipMapper.php index 8f6af16b267..cd722b64f04 100644 --- a/apps/user_ldap/lib/Db/GroupMembershipMapper.php +++ b/apps/user_ldap/lib/Db/GroupMembershipMapper.php @@ -64,6 +64,18 @@ class GroupMembershipMapper extends QBMapper { 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/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index fd2945106bc..5ad64753c0c 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -61,9 +61,9 @@ use function json_decode; class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend { protected bool $enabled = false; - /** @var CappedMemoryCache<string[]> $cachedGroupMembers array of users with gid as key */ + /** @var CappedMemoryCache<string[]> $cachedGroupMembers array of user DN with gid as key */ protected CappedMemoryCache $cachedGroupMembers; - /** @var CappedMemoryCache<array[]> $cachedGroupsByMember array of groups with uid as key */ + /** @var CappedMemoryCache<array[]> $cachedGroupsByMember array of groups with user DN as key */ protected CappedMemoryCache $cachedGroupsByMember; /** @var CappedMemoryCache<string[]> $cachedNestedGroups array of groups with gid (DN) as key */ protected CappedMemoryCache $cachedNestedGroups; @@ -1413,4 +1413,35 @@ class Group_LDAP extends ABackend implements GroupInterface, IGroupLDAP, IGetDis 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 ed4b47d8534..a5e5c6c1413 100644 --- a/apps/user_ldap/lib/Group_Proxy.php +++ b/apps/user_ldap/lib/Group_Proxy.php @@ -392,4 +392,8 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet 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 new file mode 100644 index 00000000000..ac5b32635c8 --- /dev/null +++ b/apps/user_ldap/lib/LoginListener.php @@ -0,0 +1,131 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023, Côme Chilliet <come.chilliet@nextcloud.com> + * + * @author Côme Chilliet <come.chilliet@nextcloud.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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<PostLoginEvent> + */ +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])); + $this->groupBackend->addRelationshipToCaches($userId, null, $groupId); + $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 + ] + ); + } + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 720803a78d1..c34cec38eb1 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -129,7 +129,7 @@ interface IRegistrationContext { * @param string $event preferably the fully-qualified class name of the Event sub class to listen for * @psalm-param string|class-string<T> $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<T>> $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) * |