Skip to content

Commit 8fc543b

Browse files
committed
feat: locally cache frequently requested LDAP mapping data
Signed-off-by: Arthur Schiwon <[email protected]>
1 parent d5220d6 commit 8fc543b

File tree

6 files changed

+119
-21
lines changed

6 files changed

+119
-21
lines changed

apps/user_ldap/ajax/clearMappings.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use OCA\User_LDAP\Mapping\GroupMapping;
99
use OCA\User_LDAP\Mapping\UserMapping;
1010
use OCP\EventDispatcher\IEventDispatcher;
11-
use OCP\IDBConnection;
1211
use OCP\IUserManager;
1312
use OCP\Server;
1413
use OCP\User\Events\BeforeUserIdUnassignedEvent;
@@ -40,7 +39,7 @@ function (string $uid) use ($dispatcher): void {
4039
}
4140
);
4241
} elseif ($subject === 'group') {
43-
$mapping = new GroupMapping(Server::get(IDBConnection::class));
42+
$mapping = Server::get(GroupMapping::class);
4443
$result = $mapping->clear();
4544
}
4645

apps/user_ldap/lib/Mapping/AbstractMapping.php

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use Doctrine\DBAL\Exception;
1111
use OCP\DB\IPreparedStatement;
1212
use OCP\DB\QueryBuilder\IQueryBuilder;
13+
use OCP\IAppConfig;
14+
use OCP\ICache;
15+
use OCP\ICacheFactory;
1316
use OCP\IDBConnection;
1417
use OCP\Server;
1518
use Psr\Log\LoggerInterface;
@@ -27,16 +30,81 @@ abstract class AbstractMapping {
2730
*/
2831
abstract protected function getTableName(bool $includePrefix = true);
2932

33+
/**
34+
* A month worth of cache time for as good as never changing mapping data.
35+
* Implemented when it was found that name-to-DN lookups are quite frequent.
36+
*/
37+
protected const LOCAL_CACHE_TTL = 2592000;
38+
39+
/**
40+
* A week worth of cache time for rarely changing user count data.
41+
*/
42+
protected const LOCAL_USER_COUNT_TTL = 604800;
43+
44+
/**
45+
* By default, the local cache is only used up to a certain amount of objects.
46+
* This constant holds this number. The amount of entries would amount up to
47+
* 1 MiB (worst case) per mappings table.
48+
* Setting `use_local_mapping_cache` for `user_ldap` to `yes` or `no`
49+
* deliberately enables or disables this mechanism.
50+
*/
51+
protected const LOCAL_CACHE_OBJECT_THRESHOLD = 2000;
52+
53+
protected ?ICache $localNameToDnCache = null;
54+
55+
/** @var array caches Names (value) by DN (key) */
56+
protected array $cache = [];
57+
3058
/**
3159
* @param IDBConnection $dbc
3260
*/
3361
public function __construct(
3462
protected IDBConnection $dbc,
63+
protected ICacheFactory $cacheFactory,
64+
protected IAppConfig $config,
65+
protected bool $isCLI,
3566
) {
67+
$this->initLocalCache();
3668
}
3769

38-
/** @var array caches Names (value) by DN (key) */
39-
protected $cache = [];
70+
protected function initLocalCache(): void {
71+
if ($this->isCLI || !$this->cacheFactory->isLocalCacheAvailable()) {
72+
return;
73+
}
74+
75+
$useLocalCache = $this->config->getValueString('user_ldap', 'use_local_mapping_cache', 'auto', false);
76+
if ($useLocalCache !== 'yes' && $useLocalCache !== 'auto') {
77+
return;
78+
}
79+
80+
$section = \str_contains($this->getTableName(), 'user') ? 'u/' : 'g/';
81+
$this->localNameToDnCache = $this->cacheFactory->createLocal('ldap/map/' . $section);
82+
83+
// We use the cache as well to store whether it shall be used. If the
84+
// answer was no, we unset it again.
85+
if ($useLocalCache === 'auto' && !$this->useCacheByUserCount()) {
86+
$this->localNameToDnCache = null;
87+
}
88+
}
89+
90+
protected function useCacheByUserCount(): bool {
91+
$use = $this->localNameToDnCache->get('use');
92+
if ($use !== null) {
93+
return $use;
94+
}
95+
96+
$qb = $this->dbc->getQueryBuilder();
97+
$q = $qb->selectAlias($qb->createFunction('COUNT(owncloud_name)'), 'count')
98+
->from($this->getTableName());
99+
$q->setMaxResults(self::LOCAL_CACHE_OBJECT_THRESHOLD + 1);
100+
$result = $q->executeQuery();
101+
$row = $result->fetch();
102+
$result->closeCursor();
103+
104+
$use = (int)$row['count'] <= self::LOCAL_CACHE_OBJECT_THRESHOLD;
105+
$this->localNameToDnCache->set('use', $use, self::LOCAL_USER_COUNT_TTL);
106+
return $use;
107+
}
40108

41109
/**
42110
* checks whether a provided string represents an existing table col
@@ -107,17 +175,22 @@ protected function modify(IPreparedStatement $statement, $parameters) {
107175

108176
/**
109177
* Gets the LDAP DN based on the provided name.
110-
* Replaces Access::ocname2dn
111-
*
112-
* @param string $name
113-
* @return string|false
114178
*/
115-
public function getDNByName($name) {
116-
$dn = array_search($name, $this->cache);
117-
if ($dn === false && ($dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name)) !== false) {
118-
$this->cache[$dn] = $name;
179+
public function getDNByName(string $name): string|false {
180+
$dn = array_search($name, $this->cache, true);
181+
if ($dn === false) {
182+
$dn = $this->localNameToDnCache?->get($name);
183+
if ($dn === null || $dn === false) {
184+
$dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name);
185+
if ($dn === false) {
186+
return false;
187+
}
188+
189+
$this->cache[$dn] = $name;
190+
$this->localNameToDnCache?->set($name, $dn, self::LOCAL_CACHE_TTL);
191+
}
119192
}
120-
return $dn;
193+
return $dn ?? false;
121194
}
122195

123196
/**
@@ -128,6 +201,7 @@ public function getDNByName($name) {
128201
* @return bool
129202
*/
130203
public function setDNbyUUID($fdn, $uuid) {
204+
// FIXME: need to update local cache by name and broadcast this information
131205
$oldDn = $this->getDnByUUID($uuid);
132206
$statement = $this->dbc->prepare('
133207
UPDATE `' . $this->getTableName() . '`
@@ -351,6 +425,7 @@ public function map($fdn, $name, $uuid) {
351425
$result = $this->dbc->insertIfNotExist($this->getTableName(), $row);
352426
if ((bool)$result === true) {
353427
$this->cache[$fdn] = $name;
428+
$this->localNameToDnCache?->set($name, $fdn, self::LOCAL_CACHE_TTL);
354429
}
355430
// insertIfNotExist returns values as int
356431
return (bool)$result;
@@ -374,6 +449,7 @@ public function unmap($name) {
374449
if ($dn !== false) {
375450
unset($this->cache[$dn]);
376451
}
452+
$this->localNameToDnCache?->remove($name);
377453

378454
return $this->modify($statement, [$name]);
379455
}
@@ -389,6 +465,7 @@ public function clear() {
389465
->getTruncateTableSQL('`' . $this->getTableName() . '`');
390466
try {
391467
$this->dbc->executeQuery($sql);
468+
$this->localNameToDnCache?->clear();
392469

393470
return true;
394471
} catch (Exception $e) {

apps/user_ldap/lib/Mapping/UserMapping.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
namespace OCA\User_LDAP\Mapping;
99

1010
use OCP\HintException;
11+
use OCP\IAppConfig;
12+
use OCP\ICacheFactory;
1113
use OCP\IDBConnection;
1214
use OCP\IRequest;
1315
use OCP\Server;
@@ -24,9 +26,12 @@ class UserMapping extends AbstractMapping {
2426

2527
public function __construct(
2628
IDBConnection $dbc,
29+
ICacheFactory $cacheFactory,
30+
IAppConfig $config,
31+
bool $isCLI,
2732
private IAssertion $assertion,
2833
) {
29-
parent::__construct($dbc);
34+
parent::__construct($dbc, $cacheFactory, $config, $isCLI);
3035
}
3136

3237
/**

apps/user_ldap/tests/Mapping/AbstractMappingTestCase.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,31 @@
99
namespace OCA\User_LDAP\Tests\Mapping;
1010

1111
use OCA\User_LDAP\Mapping\AbstractMapping;
12+
use OCP\IAppConfig;
13+
use OCP\ICacheFactory;
1214
use OCP\IDBConnection;
1315
use OCP\Server;
16+
use PHPUnit\Framework\MockObject\MockObject;
1417

1518
abstract class AbstractMappingTestCase extends \Test\TestCase {
16-
abstract public function getMapper(IDBConnection $dbMock);
19+
20+
private ICacheFactory&MockObject $cacheFactoryMock;
21+
22+
private IAppConfig&MockObject $configMock;
23+
24+
abstract public function getMapper(IDBConnection $dbMock, ICacheFactory $cacheFactory, IAppConfig $appConfig): AbstractMapping;
25+
26+
protected function setUp(): void {
27+
$this->cacheFactoryMock = $this->createMock(ICacheFactory::class);
28+
$this->configMock = $this->createMock(IAppConfig::class);
29+
}
1730

1831
/**
1932
* kiss test on isColNameValid
2033
*/
2134
public function testIsColNameValid(): void {
2235
$dbMock = $this->createMock(IDBConnection::class);
23-
$mapper = $this->getMapper($dbMock);
36+
$mapper = $this->getMapper($dbMock, $this->cacheFactoryMock, $this->configMock);
2437

2538
$this->assertTrue($mapper->isColNameValid('ldap_dn'));
2639
$this->assertFalse($mapper->isColNameValid('foobar'));
@@ -71,7 +84,7 @@ protected function mapEntries(AbstractMapping $mapper, array $data): void {
7184
*/
7285
private function initTest(): array {
7386
$dbc = Server::get(IDBConnection::class);
74-
$mapper = $this->getMapper($dbc);
87+
$mapper = $this->getMapper($dbc, $this->cacheFactoryMock, $this->configMock);
7588
$data = $this->getTestData();
7689
// make sure DB is pristine, then fill it with test entries
7790
$mapper->clear();

apps/user_ldap/tests/Mapping/GroupMappingTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
namespace OCA\User_LDAP\Tests\Mapping;
1010

1111
use OCA\User_LDAP\Mapping\GroupMapping;
12+
use OCP\IAppConfig;
13+
use OCP\ICacheFactory;
1214
use OCP\IDBConnection;
1315

1416
/**
@@ -19,7 +21,7 @@
1921
* @package OCA\User_LDAP\Tests\Mapping
2022
*/
2123
class GroupMappingTest extends AbstractMappingTestCase {
22-
public function getMapper(IDBConnection $dbMock) {
23-
return new GroupMapping($dbMock);
24+
public function getMapper(IDBConnection $dbMock, ICacheFactory $cacheFactory, IAppConfig $appConfig): GroupMapping {
25+
return new GroupMapping($dbMock, $cacheFactory, $appConfig, true);
2426
}
2527
}

apps/user_ldap/tests/Mapping/UserMappingTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
namespace OCA\User_LDAP\Tests\Mapping;
1010

1111
use OCA\User_LDAP\Mapping\UserMapping;
12+
use OCP\IAppConfig;
13+
use OCP\ICacheFactory;
1214
use OCP\IDBConnection;
1315
use OCP\Support\Subscription\IAssertion;
1416

@@ -20,7 +22,7 @@
2022
* @package OCA\User_LDAP\Tests\Mapping
2123
*/
2224
class UserMappingTest extends AbstractMappingTestCase {
23-
public function getMapper(IDBConnection $dbMock) {
24-
return new UserMapping($dbMock, $this->createMock(IAssertion::class));
25+
public function getMapper(IDBConnection $dbMock, ICacheFactory $cacheFactory, IAppConfig $appConfig): UserMapping {
26+
return new UserMapping($dbMock, $cacheFactory, $appConfig, true, $this->createMock(IAssertion::class));
2527
}
2628
}

0 commit comments

Comments
 (0)