prepare-dovecot-script.test.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import assert from 'node:assert/strict';
  2. import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs';
  3. import { tmpdir } from 'node:os';
  4. import path from 'node:path';
  5. import { spawnSync } from 'node:child_process';
  6. import { test } from 'node:test';
  7. test('Dovecot preparation creates a private secret and Maildir root', () => {
  8. const projectDir = mkdtempSync(path.join(tmpdir(), 'mailhub-dovecot-prepare-'));
  9. const result = spawnSync('bash', ['scripts/prepare-dovecot.sh'], {
  10. cwd: path.resolve(import.meta.dirname, '..'),
  11. env: {
  12. ...process.env,
  13. MAILHUB_PROJECT_DIR: projectDir
  14. },
  15. encoding: 'utf8'
  16. });
  17. const unsupportedLinuxUid = process.platform === 'linux'
  18. && typeof process.getuid === 'function'
  19. && ![0, 1000].includes(process.getuid());
  20. if (unsupportedLinuxUid) {
  21. assert.notEqual(result.status, 0);
  22. assert.match(result.stderr, /must run as root or host uid 1000/);
  23. return;
  24. }
  25. assert.equal(result.status, 0, result.stderr || result.stdout);
  26. const secretPath = path.join(projectDir, 'data', 'secrets', 'dovecot_auth_secret');
  27. const webmailSecretPath = path.join(projectDir, 'data', 'secrets', 'webmail_sso_secret');
  28. const maildirRoot = path.join(projectDir, 'data', 'maildir');
  29. const secretStat = statSync(secretPath);
  30. assert.equal(secretStat.isFile(), true);
  31. assert.equal(secretStat.mode & 0o777, 0o400);
  32. assert.equal(statSync(webmailSecretPath).isFile(), true);
  33. assert.equal(statSync(webmailSecretPath).mode & 0o777, 0o440);
  34. assert.equal(statSync(path.dirname(webmailSecretPath)).mode & 0o777, 0o750);
  35. assert.notEqual(readFileSync(secretPath, 'utf8'), readFileSync(webmailSecretPath, 'utf8'));
  36. assert.equal(statSync(maildirRoot).isDirectory(), true);
  37. assert.equal(statSync(maildirRoot).mode & 0o777, 0o700);
  38. });
  39. test('Dovecot preparation grants only the configured Roundcube worker gid', () => {
  40. const projectDir = mkdtempSync(path.join(tmpdir(), 'mailhub-webmail-reader-'));
  41. const binDir = path.join(projectDir, 'bin');
  42. const chownLog = path.join(projectDir, 'chown.log');
  43. mkdirSync(binDir);
  44. writeExecutable(path.join(binDir, 'id'), '#!/usr/bin/env bash\necho 0\n');
  45. writeExecutable(path.join(binDir, 'uname'), '#!/usr/bin/env bash\necho Darwin\n');
  46. writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
  47. printf '%s\\n' "$*" >> "\${CHOWN_LOG}"
  48. `);
  49. writeFileSync(path.join(projectDir, '.env'), 'WEBMAIL_SSO_READER_GID=33\n');
  50. const result = spawnSync('bash', ['scripts/prepare-dovecot.sh'], {
  51. cwd: path.resolve(import.meta.dirname, '..'),
  52. env: {
  53. ...process.env,
  54. PATH: `${binDir}:${process.env.PATH}`,
  55. CHOWN_LOG: chownLog,
  56. MAILHUB_PROJECT_DIR: projectDir
  57. },
  58. encoding: 'utf8'
  59. });
  60. assert.equal(result.status, 0, result.stderr || result.stdout);
  61. const chownCalls = readFileSync(chownLog, 'utf8').trim().split('\n');
  62. const privateCall = chownCalls.find((line) => line.startsWith('1000:1000 '));
  63. const webmailCall = chownCalls.find((line) => line.startsWith('1000:33 '));
  64. assert.ok(privateCall?.includes('dovecot_auth_secret'));
  65. assert.ok(privateCall?.includes('/maildir'));
  66. assert.ok(!privateCall?.includes('webmail_sso_secret'));
  67. assert.ok(webmailCall?.includes('/secrets'));
  68. assert.ok(webmailCall?.includes('webmail_sso_secret'));
  69. });
  70. test('Dovecot preparation rejects ambiguous or invalid Roundcube reader gids', () => {
  71. for (const envContents of [
  72. 'WEBMAIL_SSO_READER_GID=33\nWEBMAIL_SSO_READER_GID=34\n',
  73. 'WEBMAIL_SSO_READER_GID=www-data\n',
  74. 'WEBMAIL_SSO_READER_GID=0\n'
  75. ]) {
  76. const projectDir = mkdtempSync(path.join(tmpdir(), 'mailhub-webmail-reader-invalid-'));
  77. writeFileSync(path.join(projectDir, '.env'), envContents);
  78. const result = spawnSync('bash', ['scripts/prepare-dovecot.sh'], {
  79. cwd: path.resolve(import.meta.dirname, '..'),
  80. env: {
  81. ...process.env,
  82. MAILHUB_PROJECT_DIR: projectDir
  83. },
  84. encoding: 'utf8'
  85. });
  86. assert.notEqual(result.status, 0);
  87. assert.match(result.stderr, /reader gid|must appear at most once/i);
  88. }
  89. });
  90. function writeExecutable(filePath, contents) {
  91. writeFileSync(filePath, contents);
  92. chmodSync(filePath, 0o755);
  93. }