aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCôme Chilliet <91878298+come-nc@users.noreply.github.com>2023-10-16 10:01:55 +0200
committerGitHub <noreply@github.com>2023-10-16 10:01:55 +0200
commit8212feefb9cf5796e6a92b27956092a1b5e933fe (patch)
tree8e38323a4e370fecfe22834b68cc7bb44dc289ea
parent356c2219bc935526ee0f304e28695552e4f0827e (diff)
parent500374a8e7528b81e882d43c06b625839208474d (diff)
downloadnextcloud-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.php1
-rw-r--r--apps/user_ldap/composer/composer/autoload_static.php1
-rw-r--r--apps/user_ldap/composer/composer/installed.php4
-rw-r--r--apps/user_ldap/lib/AppInfo/Application.php5
-rw-r--r--apps/user_ldap/lib/Db/GroupMembershipMapper.php12
-rw-r--r--apps/user_ldap/lib/Group_LDAP.php35
-rw-r--r--apps/user_ldap/lib/Group_Proxy.php4
-rw-r--r--apps/user_ldap/lib/LoginListener.php131
-rw-r--r--lib/public/AppFramework/Bootstrap/IRegistrationContext.php2
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)
*