|
|
@@ -1,5 +1,5 @@
|
|
|
import assert from 'node:assert/strict';
|
|
|
-import { mkdtempSync, readFileSync, statSync } from 'node:fs';
|
|
|
+import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
|
import { tmpdir } from 'node:os';
|
|
|
import path from 'node:path';
|
|
|
import { spawnSync } from 'node:child_process';
|
|
|
@@ -38,3 +38,63 @@ test('Dovecot preparation creates a private secret and Maildir root', () => {
|
|
|
assert.equal(statSync(maildirRoot).isDirectory(), true);
|
|
|
assert.equal(statSync(maildirRoot).mode & 0o777, 0o700);
|
|
|
});
|
|
|
+
|
|
|
+test('Dovecot preparation grants only the configured Roundcube worker gid', () => {
|
|
|
+ const projectDir = mkdtempSync(path.join(tmpdir(), 'mailhub-webmail-reader-'));
|
|
|
+ const binDir = path.join(projectDir, 'bin');
|
|
|
+ const chownLog = path.join(projectDir, 'chown.log');
|
|
|
+ mkdirSync(binDir);
|
|
|
+ writeExecutable(path.join(binDir, 'id'), '#!/usr/bin/env bash\necho 0\n');
|
|
|
+ writeExecutable(path.join(binDir, 'uname'), '#!/usr/bin/env bash\necho Darwin\n');
|
|
|
+ writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
|
|
|
+printf '%s\\n' "$*" >> "\${CHOWN_LOG}"
|
|
|
+`);
|
|
|
+ writeFileSync(path.join(projectDir, '.env'), 'WEBMAIL_SSO_READER_GID=33\n');
|
|
|
+
|
|
|
+ const result = spawnSync('bash', ['scripts/prepare-dovecot.sh'], {
|
|
|
+ cwd: path.resolve(import.meta.dirname, '..'),
|
|
|
+ env: {
|
|
|
+ ...process.env,
|
|
|
+ PATH: `${binDir}:${process.env.PATH}`,
|
|
|
+ CHOWN_LOG: chownLog,
|
|
|
+ MAILHUB_PROJECT_DIR: projectDir
|
|
|
+ },
|
|
|
+ encoding: 'utf8'
|
|
|
+ });
|
|
|
+ assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
|
+
|
|
|
+ const chownCalls = readFileSync(chownLog, 'utf8').trim().split('\n');
|
|
|
+ const privateCall = chownCalls.find((line) => line.startsWith('1000:1000 '));
|
|
|
+ const webmailCall = chownCalls.find((line) => line.startsWith('1000:33 '));
|
|
|
+ assert.ok(privateCall?.includes('dovecot_auth_secret'));
|
|
|
+ assert.ok(privateCall?.includes('/maildir'));
|
|
|
+ assert.ok(!privateCall?.includes('webmail_sso_secret'));
|
|
|
+ assert.ok(webmailCall?.includes('/secrets'));
|
|
|
+ assert.ok(webmailCall?.includes('webmail_sso_secret'));
|
|
|
+});
|
|
|
+
|
|
|
+test('Dovecot preparation rejects ambiguous or invalid Roundcube reader gids', () => {
|
|
|
+ for (const envContents of [
|
|
|
+ 'WEBMAIL_SSO_READER_GID=33\nWEBMAIL_SSO_READER_GID=34\n',
|
|
|
+ 'WEBMAIL_SSO_READER_GID=www-data\n',
|
|
|
+ 'WEBMAIL_SSO_READER_GID=0\n'
|
|
|
+ ]) {
|
|
|
+ const projectDir = mkdtempSync(path.join(tmpdir(), 'mailhub-webmail-reader-invalid-'));
|
|
|
+ writeFileSync(path.join(projectDir, '.env'), envContents);
|
|
|
+ const result = spawnSync('bash', ['scripts/prepare-dovecot.sh'], {
|
|
|
+ cwd: path.resolve(import.meta.dirname, '..'),
|
|
|
+ env: {
|
|
|
+ ...process.env,
|
|
|
+ MAILHUB_PROJECT_DIR: projectDir
|
|
|
+ },
|
|
|
+ encoding: 'utf8'
|
|
|
+ });
|
|
|
+ assert.notEqual(result.status, 0);
|
|
|
+ assert.match(result.stderr, /reader gid|must appear at most once/i);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+function writeExecutable(filePath, contents) {
|
|
|
+ writeFileSync(filePath, contents);
|
|
|
+ chmodSync(filePath, 0o755);
|
|
|
+}
|