Skip to content

Commit dee73ac

Browse files
blizzzjuliusknorr
authored andcommitted
fix(LDAP): solve race condition reading groups of disappeared LDAP user
Signed-off-by: Arthur Schiwon <[email protected]>
1 parent 29a2daf commit dee73ac

File tree

3 files changed

+131
-7
lines changed

3 files changed

+131
-7
lines changed

apps/user_ldap/lib/Group_LDAP.php

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use OCP\Group\Backend\IGetDisplayNameBackend;
5353
use OC\ServerNotAvailableException;
5454
use OCP\IConfig;
55+
use OCP\IUserManager;
5556
use OCP\Server;
5657
use Psr\Log\LoggerInterface;
5758
use function json_decode;
@@ -73,8 +74,14 @@ class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, I
7374
*/
7475
protected string $ldapGroupMemberAssocAttr;
7576
private IConfig $config;
76-
77-
public function __construct(Access $access, GroupPluginManager $groupPluginManager, IConfig $config) {
77+
private IUserManager $ncUserManager;
78+
79+
public function __construct(
80+
Access $access,
81+
GroupPluginManager $groupPluginManager,
82+
IConfig $config,
83+
IUserManager $ncUserManager
84+
) {
7885
parent::__construct($access);
7986
$filter = $this->access->connection->ldapGroupFilter;
8087
$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
@@ -89,6 +96,7 @@ public function __construct(Access $access, GroupPluginManager $groupPluginManag
8996
$this->logger = Server::get(LoggerInterface::class);
9097
$this->ldapGroupMemberAssocAttr = strtolower((string)$gAssoc);
9198
$this->config = $config;
99+
$this->ncUserManager = $ncUserManager;
92100
}
93101

94102
/**
@@ -443,6 +451,7 @@ public function getGroupGidNumber(string $dn) {
443451
public function getUserGidNumber(string $dn) {
444452
$gidNumber = false;
445453
if ($this->access->connection->hasGidNumber) {
454+
// FIXME: when $dn does not exist on LDAP anymore, this will be set wrongly to false :/
446455
$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
447456
if ($gidNumber === false) {
448457
$this->access->connection->hasGidNumber = false;
@@ -656,6 +665,25 @@ public function getUserPrimaryGroup(string $dn) {
656665
return false;
657666
}
658667

668+
private function isUserOnLDAP(string $uid): bool {
669+
// forces a user exists check - but does not help if a positive result is cached, while group info is not
670+
$ncUser = $this->ncUserManager->get($uid);
671+
if ($ncUser === null) {
672+
return false;
673+
}
674+
$backend = $ncUser->getBackend();
675+
if ($backend instanceof User_Proxy) {
676+
// ignoring cache as safeguard (and we are behind the group cache check anyway)
677+
return $backend->userExistsOnLDAP($uid, true);
678+
}
679+
return false;
680+
}
681+
682+
protected function getCachedGroupsForUserId(string $uid): array {
683+
$groupStr = $this->config->getUserValue($uid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), '[]');
684+
return json_decode($groupStr) ?? [];
685+
}
686+
659687
/**
660688
* This function fetches all groups a user belongs to. It does not check
661689
* if the user exists at all.
@@ -683,8 +711,7 @@ public function getUserGroups($uid): array {
683711
if ($user instanceof OfflineUser) {
684712
// We load known group memberships from configuration for remnants,
685713
// because LDAP server does not contain them anymore
686-
$groupStr = $this->config->getUserValue($uid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), '[]');
687-
return json_decode($groupStr) ?? [];
714+
return $this->getCachedGroupsForUserId($uid);
688715
}
689716

690717
$userDN = $this->access->username2dn($uid);
@@ -798,8 +825,18 @@ public function getUserGroups($uid): array {
798825
$groups[] = $gidGroupName;
799826
}
800827

828+
if (empty($groups) && !$this->isUserOnLDAP($ncUid)) {
829+
// Groups are enabled, but you user has none? Potentially suspicious:
830+
// it could be that the user was deleted from LDAP, but we are not
831+
// aware of it yet.
832+
$groups = $this->getCachedGroupsForUserId($ncUid);
833+
$this->access->connection->writeToCache($cacheKey, $groups);
834+
return $groups;
835+
}
836+
801837
$groups = array_unique($groups, SORT_LOCALE_STRING);
802838
$this->access->connection->writeToCache($cacheKey, $groups);
839+
803840
$groupStr = \json_encode($groups);
804841
$this->config->setUserValue($ncUid, 'user_ldap', 'cached-group-memberships-' . $this->access->connection->getConfigPrefix(), $groupStr);
805842

apps/user_ldap/lib/Group_Proxy.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use OCP\Group\Backend\INamedBackend;
3434
use OCP\GroupInterface;
3535
use OCP\IConfig;
36+
use OCP\IUserManager;
3637

3738
class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend {
3839
private $backends = [];
@@ -41,18 +42,21 @@ class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGet
4142
private GroupPluginManager $groupPluginManager;
4243
private bool $isSetUp = false;
4344
private IConfig $config;
45+
private IUserManager $ncUserManager;
4446

4547
public function __construct(
4648
Helper $helper,
4749
ILDAPWrapper $ldap,
4850
AccessFactory $accessFactory,
4951
GroupPluginManager $groupPluginManager,
5052
IConfig $config,
53+
IUserManager $ncUserManager,
5154
) {
5255
parent::__construct($ldap, $accessFactory);
5356
$this->helper = $helper;
5457
$this->groupPluginManager = $groupPluginManager;
5558
$this->config = $config;
59+
$this->ncUserManager = $ncUserManager;
5660
}
5761

5862
protected function setup(): void {
@@ -63,7 +67,7 @@ protected function setup(): void {
6367
$serverConfigPrefixes = $this->helper->getServerConfigurationPrefixes(true);
6468
foreach ($serverConfigPrefixes as $configPrefix) {
6569
$this->backends[$configPrefix] =
66-
new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config);
70+
new Group_LDAP($this->getAccess($configPrefix), $this->groupPluginManager, $this->config, $this->ncUserManager);
6771
if (is_null($this->refBackend)) {
6872
$this->refBackend = &$this->backends[$configPrefix];
6973
}

apps/user_ldap/tests/Group_LDAPTest.php

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@
3838
use OCA\User_LDAP\Mapping\GroupMapping;
3939
use OCA\User_LDAP\User\Manager;
4040
use OCA\User_LDAP\User\OfflineUser;
41+
use OCA\User_LDAP\User\User;
42+
use OCA\User_LDAP\User_Proxy;
4143
use OCP\GroupInterface;
4244
use OCP\IConfig;
45+
use OCP\IUser;
46+
use OCP\IUserManager;
4347
use PHPUnit\Framework\MockObject\MockObject;
4448
use Test\TestCase;
4549

@@ -54,6 +58,7 @@ class Group_LDAPTest extends TestCase {
5458
private MockObject|Access $access;
5559
private MockObject|GroupPluginManager $pluginManager;
5660
private MockObject|IConfig $config;
61+
private MockObject|IUserManager $ncUserManager;
5762
private GroupLDAP $groupBackend;
5863

5964
public function setUp(): void {
@@ -62,10 +67,11 @@ public function setUp(): void {
6267
$this->access = $this->getAccessMock();
6368
$this->pluginManager = $this->createMock(GroupPluginManager::class);
6469
$this->config = $this->createMock(IConfig::class);
70+
$this->ncUserManager = $this->createMock(IUserManager::class);
6571
}
6672

6773
public function initBackend(): void {
68-
$this->groupBackend = new GroupLDAP($this->access, $this->pluginManager, $this->config);
74+
$this->groupBackend = new GroupLDAP($this->access, $this->pluginManager, $this->config, $this->ncUserManager);
6975
}
7076

7177
public function testCountEmptySearchString() {
@@ -786,12 +792,14 @@ public function testGetUserGroupsMemberOf() {
786792
$this->access->connection->hasPrimaryGroups = false;
787793
$this->access->connection->hasGidNumber = false;
788794

795+
$expectedGroups = ['cn=groupA,dc=foobar', 'cn=groupB,dc=foobar'];
796+
789797
$this->access->expects($this->any())
790798
->method('username2dn')
791799
->willReturn($dn);
792800
$this->access->expects($this->exactly(5))
793801
->method('readAttribute')
794-
->will($this->onConsecutiveCalls(['cn=groupA,dc=foobar', 'cn=groupB,dc=foobar'], [], [], [], []));
802+
->will($this->onConsecutiveCalls($expectedGroups, [], [], [], []));
795803
$this->access->expects($this->any())
796804
->method('dn2groupname')
797805
->willReturnArgument(0);
@@ -802,6 +810,10 @@ public function testGetUserGroupsMemberOf() {
802810
->method('isDNPartOfBase')
803811
->willReturn(true);
804812

813+
$this->config->expects($this->once())
814+
->method('setUserValue')
815+
->with('userX', 'user_ldap', 'cached-group-memberships-', \json_encode($expectedGroups));
816+
805817
$this->initBackend();
806818
$groups = $this->groupBackend->getUserGroups('userX');
807819

@@ -835,6 +847,34 @@ public function testGetUserGroupsMemberOfDisabled() {
835847
->method('nextcloudGroupNames')
836848
->willReturn([]);
837849

850+
// empty group result should not be oer
851+
$this->config->expects($this->once())
852+
->method('setUserValue')
853+
->with('userX', 'user_ldap', 'cached-group-memberships-', '[]');
854+
855+
$ldapUser = $this->createMock(User::class);
856+
857+
$this->access->userManager->expects($this->any())
858+
->method('get')
859+
->with('userX')
860+
->willReturn($ldapUser);
861+
862+
$userBackend = $this->createMock(User_Proxy::class);
863+
$userBackend->expects($this->once())
864+
->method('userExistsOnLDAP')
865+
->with('userX', true)
866+
->willReturn(true);
867+
868+
$ncUser = $this->createMock(IUser::class);
869+
$ncUser->expects($this->any())
870+
->method('getBackend')
871+
->willReturn($userBackend);
872+
873+
$this->ncUserManager->expects($this->once())
874+
->method('get')
875+
->with('userX')
876+
->willReturn($ncUser);
877+
838878
$this->initBackend();
839879
$this->groupBackend->getUserGroups('userX');
840880
}
@@ -861,6 +901,49 @@ public function testGetUserGroupsOfflineUser() {
861901
$this->assertTrue(in_array('groupF', $returnedGroups));
862902
}
863903

904+
public function testGetUserGroupsUnrecognizedOfflineUser() {
905+
$this->enableGroups();
906+
$dn = 'cn=userX,dc=foobar';
907+
908+
$ldapUser = $this->createMock(User::class);
909+
910+
$userBackend = $this->createMock(User_Proxy::class);
911+
$userBackend->expects($this->once())
912+
->method('userExistsOnLDAP')
913+
->with('userX', true)
914+
->willReturn(false);
915+
916+
$ncUser = $this->createMock(IUser::class);
917+
$ncUser->expects($this->any())
918+
->method('getBackend')
919+
->willReturn($userBackend);
920+
921+
$this->config->expects($this->atLeastOnce())
922+
->method('getUserValue')
923+
->with('userX', 'user_ldap', 'cached-group-memberships-', $this->anything())
924+
->willReturn(\json_encode(['groupB', 'groupF']));
925+
926+
$this->access->expects($this->any())
927+
->method('username2dn')
928+
->willReturn($dn);
929+
930+
$this->access->userManager->expects($this->any())
931+
->method('get')
932+
->with('userX')
933+
->willReturn($ldapUser);
934+
935+
$this->ncUserManager->expects($this->once())
936+
->method('get')
937+
->with('userX')
938+
->willReturn($ncUser);
939+
940+
$this->initBackend();
941+
$returnedGroups = $this->groupBackend->getUserGroups('userX');
942+
$this->assertCount(2, $returnedGroups);
943+
$this->assertTrue(in_array('groupB', $returnedGroups));
944+
$this->assertTrue(in_array('groupF', $returnedGroups));
945+
}
946+
864947
public function nestedGroupsProvider(): array {
865948
return [
866949
[true],

0 commit comments

Comments
 (0)