mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
A refused login used to delete the account's data, and with it everyone's S/MIME and OpenPGP keys, also with offline off. The usual cause is a password changed on another device, which Stalwart 0.16 and 1.0 both answer with `invalid_grant`, so that lost unsent mail for little protection. - A refused login signs out at once and hides the data. A new password brings it back, unsent mail included. Otherwise it goes after 30 days. The sign-in page gives a count and a date. The 24-hour timer is gone. - `organisation.devices.wipeOnRefusedLogin`, off by default, asks for deletion at once, of that account's own data only. - The session limit no longer counts as a refusal on any route. It hides the data and says nothing. - A wipe runs under a Web Lock, tells other windows when it starts and ends, and queues its report before deleting anything. It reports only once the deletion worked and no plugin database is left. - More goes with a wipe: Pro tabs, search history, Files and WebDAV state, templates, pop-out drafts, shown notifications, push state, and Web Push with the last account. - The unsent count is per account, and unknown rather than 0 when the store can't say. A new row is checked against the account that asked, and a sign-out drops its row. Existing tests changed because the rule did: `auth-store-keep-local.test.ts` expected a wipe on `invalid_grant` and at the session limit, and `device-wipe.test.ts` expected shared keys to go on a refused login.
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
|
|
// Deleting everything of one user, which only makes sense where the server and its
|
|
// files are on the device: the desktop app. On an instance the same request is a
|
|
// way to delete someone else's connected accounts, so it has to do nothing.
|
|
|
|
const deleted = vi.hoisted(() => [] as unknown[]);
|
|
const session = vi.hoisted(() => ({ signedIn: false }));
|
|
vi.mock('@/lib/stalwart/credentials', () => ({
|
|
getStalwartCredentials: async () => (session.signedIn ? { accountName: 'me@example.org', serverUrl: 'https://mail.example.org', authHeader: 'x' } : null),
|
|
}));
|
|
vi.mock('@/lib/admin/config-manager', () => ({
|
|
configManager: { ensureLoaded: async () => {}, get: () => undefined, getPolicy: () => ({ features: { pluginsEnabled: true } }) },
|
|
}));
|
|
vi.mock('@/lib/plugin-secrets/vault', () => ({
|
|
VaultFullError: class extends Error {},
|
|
deleteAllUserCredentials: vi.fn(async (user: unknown) => { deleted.push(user); }),
|
|
deleteUserCredentials: vi.fn(async () => {}),
|
|
listUserCredentials: vi.fn(async () => []),
|
|
putUserCredential: vi.fn(async () => {}),
|
|
}));
|
|
|
|
async function remove(query: string, headers: Record<string, string> = { 'sec-fetch-site': 'same-origin' }): Promise<Response> {
|
|
vi.resetModules();
|
|
const { DELETE } = await import('../route');
|
|
return DELETE(new NextRequest(`https://app.local/api/plugin-secrets?${query}`, { method: 'DELETE', headers }));
|
|
}
|
|
|
|
const EVICTED = 'all=true&username=alice&server=https%3A%2F%2Fmail.example.org';
|
|
|
|
beforeEach(() => {
|
|
deleted.length = 0;
|
|
session.signedIn = false;
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('DELETE /api/plugin-secrets?all=true', () => {
|
|
it('in the desktop app takes a dropped account by who it was, with no cookie', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1');
|
|
expect((await remove(EVICTED)).status).toBe(200);
|
|
expect(deleted).toEqual([{ username: 'alice', serverUrl: 'https://mail.example.org' }]);
|
|
});
|
|
|
|
it('won\'t take it from another site', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1');
|
|
expect((await remove(EVICTED, { 'sec-fetch-site': 'cross-site' })).status).toBe(403);
|
|
expect(deleted).toEqual([]);
|
|
});
|
|
|
|
it('on an instance deletes nobody\'s, with a cookie or without', async () => {
|
|
expect((await remove(EVICTED)).status).toBe(401);
|
|
session.signedIn = true;
|
|
await remove(EVICTED);
|
|
expect(deleted).toEqual([]);
|
|
});
|
|
|
|
it('in the desktop app with a cookie and no name takes the signed-in account', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1');
|
|
session.signedIn = true;
|
|
expect((await remove('all=true')).status).toBe(200);
|
|
expect(deleted).toEqual([{ username: 'me@example.org', serverUrl: 'https://mail.example.org' }]);
|
|
});
|
|
});
|