Fire group membership events from LDAP at logintags/v28.0.0beta1
@@ -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', |
@@ -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', |
@@ -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(), |
@@ -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) { |
@@ -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()) |
@@ -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; | |||
} | |||
} |
@@ -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]); | |||
} | |||
} |
@@ -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 | |||
] | |||
); | |||
} | |||
} | |||
} |
@@ -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) | |||
* |