Skip to content

Commit fa30be3

Browse files
committed
fix(security): read the API key via IAppConfig, not IConfig
Regression in the sensitive-api-key change: EtherpadClient::getApiKey() read the key through IConfig::getAppValue, which does NOT decrypt a value stored sensitive (encrypted at rest). So every real Etherpad call (createGroup, createAuthorIfNotExistsFor, createSession, …) sent an unusable key and got HTTP 401 — opening any pad failed with "Etherpad API request failed". The admin health-check kept passing because it reads the key via AdminSettingsRepository::getStoredSettings() → IAppConfig (which decrypts), masking the bug; only the EtherpadClient path was broken. Inject IAppConfig into EtherpadClient and read the key via getValueString. Host/api-version stay on IConfig (not sensitive). Verified on a real NC 33 install with the key stored sensitive: the pad-author-display-name and pad-create-public e2e specs (which open real pads through EtherpadClient) now pass; they failed with the 401 before this fix.
1 parent 2735e8d commit fa30be3

2 files changed

Lines changed: 25 additions & 11 deletions

File tree

lib/Service/EtherpadClient.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace OCA\EtherpadNextcloud\Service;
1010

1111
use OCA\EtherpadNextcloud\Exception\EtherpadClientException;
12+
use OCP\IAppConfig;
1213
use OCP\IConfig;
1314

1415
class EtherpadClient {
@@ -18,6 +19,7 @@ class EtherpadClient {
1819

1920
public function __construct(
2021
private IConfig $config,
22+
private IAppConfig $appConfig,
2123
) {
2224
}
2325

@@ -297,10 +299,12 @@ private function getApiHost(): string {
297299
}
298300

299301
private function getApiKey(): string {
300-
// Read via IConfig (which transparently decrypts the now-sensitive
301-
// value); the key name is owned by AdminSettingsRepository so the
302-
// write and read paths can't drift apart.
303-
$key = (string)$this->config->getAppValue('etherpad_nextcloud', AdminSettingsRepository::API_KEY, '');
302+
// Must read via IAppConfig: the key is stored sensitive (encrypted
303+
// at rest) and IConfig::getAppValue does NOT decrypt sensitive
304+
// values, so reading it through IConfig yields an unusable key and
305+
// every Etherpad call 401s. IAppConfig::getValueString decrypts it.
306+
// The key name is owned by AdminSettingsRepository.
307+
$key = $this->appConfig->getValueString('etherpad_nextcloud', AdminSettingsRepository::API_KEY, '');
304308
if ($key === '') {
305309
throw new EtherpadClientException('Etherpad API key is not configured.');
306310
}

tests/phpunit/unit/EtherpadClientTest.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace OCA\EtherpadNextcloud\Tests\Unit;
66

77
use OCA\EtherpadNextcloud\Service\EtherpadClient;
8+
use OCP\IAppConfig;
89
use OCP\IConfig;
910
use PHPUnit\Framework\TestCase;
1011

@@ -20,44 +21,53 @@ static function (string $appName, string $key, string $default = ''): string {
2021
}
2122
);
2223

23-
$client = new EtherpadClient($config);
24+
$client = $this->client($config);
2425
$this->assertSame(
2526
'https://pad.example.test/p/g.group%24pad-name',
2627
$client->buildPadUrl('g.group$pad-name')
2728
);
2829
}
2930

3031
public function testGetConfiguredOriginNormalizesScheme(): void {
31-
$client = new EtherpadClient($this->configWithHost('HTTPS://Pad.Example.Test/'));
32+
$client = $this->client($this->configWithHost('HTTPS://Pad.Example.Test/'));
3233
$this->assertSame('https://pad.example.test', $client->getConfiguredOrigin());
3334
}
3435

3536
public function testGetConfiguredOriginOmitsDefaultPorts(): void {
36-
$client = new EtherpadClient($this->configWithHost('https://pad.example.test:443'));
37+
$client = $this->client($this->configWithHost('https://pad.example.test:443'));
3738
$this->assertSame('https://pad.example.test', $client->getConfiguredOrigin());
3839

39-
$client = new EtherpadClient($this->configWithHost('http://pad.example.test:80'));
40+
$client = $this->client($this->configWithHost('http://pad.example.test:80'));
4041
$this->assertSame('http://pad.example.test', $client->getConfiguredOrigin());
4142
}
4243

4344
public function testGetConfiguredOriginKeepsNonDefaultPort(): void {
44-
$client = new EtherpadClient($this->configWithHost('https://pad.example.test:9001'));
45+
$client = $this->client($this->configWithHost('https://pad.example.test:9001'));
4546
$this->assertSame('https://pad.example.test:9001', $client->getConfiguredOrigin());
4647
}
4748

4849
public function testGetConfiguredOriginAllowsHttp(): void {
4950
// Unlike `parsePublicPadUrl`, the configured-origin accessor must not
5051
// enforce https — admins may legitimately run Etherpad on http behind
5152
// a private network.
52-
$client = new EtherpadClient($this->configWithHost('http://pad.internal.lan'));
53+
$client = $this->client($this->configWithHost('http://pad.internal.lan'));
5354
$this->assertSame('http://pad.internal.lan', $client->getConfiguredOrigin());
5455
}
5556

5657
public function testGetConfiguredOriginReturnsEmptyWhenUnconfigured(): void {
57-
$client = new EtherpadClient($this->configWithHost(''));
58+
$client = $this->client($this->configWithHost(''));
5859
$this->assertSame('', $client->getConfiguredOrigin());
5960
}
6061

62+
/**
63+
* Construct the client with a bare IAppConfig mock. None of these tests
64+
* exercise the API-key read path (no network calls), so the default
65+
* empty getValueString return is fine.
66+
*/
67+
private function client(IConfig $config): EtherpadClient {
68+
return new EtherpadClient($config, $this->createMock(IAppConfig::class));
69+
}
70+
6171
private function configWithHost(string $host): IConfig {
6272
$config = $this->createMock(IConfig::class);
6373
$config->method('getAppValue')->willReturnCallback(

0 commit comments

Comments
 (0)