Browse Source

Merge pull request #40367 from nextcloud/fix/user_ldap-update-groups-on-login

Fire group membership events from LDAP at login
tags/v28.0.0beta1
Côme Chilliet 6 months ago
parent
commit
8212feefb9
No account linked to committer's email address

+ 1
- 0
apps/user_ldap/composer/composer/autoload_classmap.php View File

@@ -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',

+ 1
- 0
apps/user_ldap/composer/composer/autoload_static.php View File

@@ -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',

+ 2
- 2
apps/user_ldap/composer/composer/installed.php View File

@@ -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(),

+ 4
- 1
apps/user_ldap/lib/AppInfo/Application.php View File

@@ -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) {

+ 12
- 0
apps/user_ldap/lib/Db/GroupMembershipMapper.php View File

@@ -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())

+ 33
- 2
apps/user_ldap/lib/Group_LDAP.php View File

@@ -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;
}
}

+ 4
- 0
apps/user_ldap/lib/Group_Proxy.php View File

@@ -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]);
}
}

+ 131
- 0
apps/user_ldap/lib/LoginListener.php View File

@@ -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
]
);
}
}
}

+ 1
- 1
lib/public/AppFramework/Bootstrap/IRegistrationContext.php View File

@@ -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)
*

Loading…
Cancel
Save