| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import assert from 'node:assert/strict';
- import { mkdtempSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import { test } from 'node:test';
- import {
- authenticateApiToken,
- createApiToken,
- createDomain,
- createInboundMailbox,
- createInboundMessage,
- createUser,
- getAdminResourceInventory,
- getApiToken,
- getInboundMessage,
- initDatabase,
- listApiTokens,
- listInboundMailboxFolders,
- listInboundMailboxes,
- revokeApiToken,
- rotateApiToken,
- searchInboundMessages,
- updateUser
- } from '../src/db.js';
- test('API tokens are recoverable only for explicit owner views and rotate without weakening authentication', () => {
- const database = initDatabase(tempDataDir(), 'api-token-recovery-secret');
- const user = createUser({
- username: 'token-owner',
- email: 'token-owner@example.com',
- password: 'password123'
- });
- const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
- const created = createApiToken(user.id, 'Receive worker', {
- scopes: ['send', 'messages:read'],
- expiresAt,
- mailboxAccess: 'owner'
- });
- assert.match(created.token, /^mh_/);
- assert.equal(created.tokenRecoverable, true);
- assert.equal(created.mailboxAccess, 'owner');
- assert.deepEqual(created.mailboxIds, []);
- const defaultListItem = listApiTokens(user.id)[0];
- assert.equal('token' in defaultListItem, false);
- assert.equal(defaultListItem.tokenRecoverable, true);
- assert.equal(listApiTokens(user.id, { includeToken: true })[0].token, created.token);
- assert.equal('token' in authenticateApiToken(created.token).token, false);
- assert.equal('token' in getAdminResourceInventory().users[0].apiTokens[0], false);
- authenticateApiToken(created.token);
- assert.ok(getApiToken(created.id, user.id).lastUsedAt);
- database.prepare('UPDATE api_tokens SET token_secret = ? WHERE id = ?').run('', created.id);
- const historical = getApiToken(created.id, user.id, { includeToken: true });
- assert.equal(historical.tokenRecoverable, false);
- assert.equal('token' in historical, false);
- assert.equal(authenticateApiToken(created.token).user.id, user.id);
- const rotated = rotateApiToken(created.id, user.id);
- assert.match(rotated.token, /^mh_/);
- assert.notEqual(rotated.token, created.token);
- assert.deepEqual(rotated.scopes, ['send', 'messages:read']);
- assert.equal(rotated.mailboxAccess, 'owner');
- assert.equal(rotated.expiresAt, expiresAt);
- assert.equal(rotated.lastUsedAt, null);
- assert.equal(authenticateApiToken(created.token), null);
- assert.equal(authenticateApiToken(rotated.token).user.id, user.id);
- assert.equal(revokeApiToken(created.id, user.id), true);
- assert.throws(() => rotateApiToken(created.id, user.id), /已撤销/);
- });
- test('API token mailbox scopes validate ownership and keep message queries fail closed', () => {
- const database = initDatabase(tempDataDir(), 'api-token-mailbox-secret');
- const admin = createUser({
- username: 'scope-admin',
- email: 'scope-admin@example.com',
- password: 'password123',
- role: 'admin'
- });
- const alice = createUser({
- username: 'scope-alice',
- email: 'scope-alice@example.com',
- password: 'password123'
- });
- const bob = createUser({
- username: 'scope-bob',
- email: 'scope-bob@example.com',
- password: 'password123'
- });
- createDomain(alice.id, domainFixture('scope-alice.example'));
- createDomain(bob.id, domainFixture('scope-bob.example'));
- const aliceMailbox = createInboundMailbox(alice.id, { address: 'inbox@scope-alice.example' });
- const bobMailbox = createInboundMailbox(bob.id, { address: 'inbox@scope-bob.example' });
- const aliceMessage = createInboundMessage(aliceMailbox, messageFixture('Alice message'));
- const bobMessage = createInboundMessage(bobMailbox, messageFixture('Bob message'));
- assert.throws(
- () => createApiToken(alice.id, 'Empty selection', {
- scopes: ['messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: []
- }),
- /至少一个/
- );
- assert.throws(
- () => createApiToken(alice.id, 'Cross-user selection', {
- scopes: ['messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: [bobMailbox.id]
- }),
- /不存在或无权访问/
- );
- assert.throws(
- () => createApiToken(alice.id, 'All mailboxes', {
- scopes: ['messages:read'],
- mailboxAccess: 'all'
- }),
- /只有管理员/
- );
- const aliceSelected = createApiToken(alice.id, 'Alice selected', {
- scopes: ['messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: [aliceMailbox.id, aliceMailbox.id]
- });
- assert.deepEqual(aliceSelected.mailboxIds, [aliceMailbox.id]);
- assert.equal(searchInboundMessages(alice.id, {}, { mailboxIds: aliceSelected.mailboxIds }).total, 1);
- assert.equal(
- searchInboundMessages(
- alice.id,
- { mailboxId: bobMailbox.id },
- { mailboxIds: aliceSelected.mailboxIds }
- ).total,
- 0
- );
- assert.equal(getInboundMessage(alice.id, bobMessage.id, { mailboxIds: aliceSelected.mailboxIds }), null);
- assert.equal(listInboundMailboxFolders(alice.id, bobMailbox.id, { mailboxIds: aliceSelected.mailboxIds }), null);
- const adminSelected = createApiToken(admin.id, 'Admin selected', {
- scopes: ['messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: [bobMailbox.id]
- });
- const selectedAccess = { includeAllUsers: true, mailboxIds: adminSelected.mailboxIds };
- const selectedMessages = searchInboundMessages(admin.id, {}, selectedAccess);
- assert.equal(selectedMessages.total, 1);
- assert.equal(selectedMessages.messages[0].id, bobMessage.id);
- assert.equal(getInboundMessage(admin.id, aliceMessage.id, selectedAccess), null);
- assert.equal(getInboundMessage(admin.id, bobMessage.id, selectedAccess).subject, 'Bob message');
- assert.equal(listInboundMailboxFolders(admin.id, aliceMailbox.id, selectedAccess), null);
- assert.equal(
- listInboundMailboxFolders(admin.id, bobMailbox.id, selectedAccess)
- .find((folder) => folder.name === 'INBOX').messageCount,
- 1
- );
- const adminAll = createApiToken(admin.id, 'Admin all', {
- scopes: ['messages:read'],
- mailboxAccess: 'all'
- });
- assert.equal(adminAll.mailboxAccess, 'all');
- assert.equal(searchInboundMessages(admin.id, {}, { includeAllUsers: true }).total, 2);
- assert.equal(listInboundMailboxes(admin.id, { includeAllUsers: true }).length, 2);
- assert.throws(() => updateUser(admin.id, { role: 'user' }), /至少保留一个/);
- createUser({
- username: 'scope-backup-admin',
- email: 'scope-backup-admin@example.com',
- password: 'password123',
- role: 'admin'
- });
- assert.equal(updateUser(admin.id, { role: 'user' }).role, 'user');
- assert.equal(authenticateApiToken(adminAll.token).user.role, 'user');
- database
- .prepare('UPDATE api_tokens SET mailbox_access_json = ? WHERE id = ?')
- .run('{"mode":"selected","mailboxIds":"broken"}', aliceSelected.id);
- const corrupted = getApiToken(aliceSelected.id, alice.id);
- assert.equal(corrupted.mailboxAccess, 'selected');
- assert.deepEqual(corrupted.mailboxIds, []);
- assert.equal(searchInboundMessages(alice.id, {}, { mailboxIds: corrupted.mailboxIds }).total, 0);
- });
- function tempDataDir() {
- return mkdtempSync(path.join(tmpdir(), 'mailhub-api-token-db-'));
- }
- function domainFixture(domain) {
- return {
- domain,
- selector: 'mh',
- verificationToken: `verify-${domain}`,
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: `mail.${domain}`,
- sendingIp: '192.0.2.44',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- };
- }
- function messageFixture(subject) {
- return {
- sender: 'sender@example.net',
- recipients: ['inbox@example.net'],
- subject,
- messageId: `<${subject.toLowerCase().replaceAll(' ', '-')}@example.net>`,
- rawMessage: `Subject: ${subject}\r\n\r\n${subject}`,
- textBody: subject
- };
- }
|