mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: ask the next user again before running plugins installed by someone who signed out
This commit is contained in:
@@ -16,6 +16,7 @@ vi.mock('@/lib/plugin-storage', () => ({
|
||||
getPreview: vi.fn().mockResolvedValue(null),
|
||||
deletePreview: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
fileStorage: { clearFiles: vi.fn().mockResolvedValue(undefined) },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/plugin-validator', () => ({
|
||||
@@ -61,7 +62,8 @@ import { register as registerActive, deregister as deregisterActive, get as getA
|
||||
// Import after mocks
|
||||
import { usePluginStore, PLUGIN_PERSIST_DELAY_MS } from '@/stores/plugin-store';
|
||||
import { pluginStorage } from '@/lib/plugin-storage';
|
||||
import { forgetSlotOffers } from '@/lib/plugin-loader';
|
||||
import { deactivatePlugin, forgetSlotOffers } from '@/lib/plugin-loader';
|
||||
import { purgeSignedOutData } from '@/lib/sign-out-cleanup';
|
||||
|
||||
function resetStore() {
|
||||
usePluginStore.setState({
|
||||
@@ -480,6 +482,35 @@ describe('plugins the admin removed', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a full sign-out', () => {
|
||||
// On a shared browser the next person used to run the previous user's
|
||||
// plugins with that user's grants and settings, without being asked.
|
||||
it("drops the user's grants and settings from plugins they installed and leaves them off", () => {
|
||||
usePluginStore.setState({
|
||||
plugins: [
|
||||
mockPlugin({
|
||||
id: 'mine', enabled: true, status: 'running', permissions: ['email:read', 'http:fetch'],
|
||||
grantedPermissions: ['email:read', 'http:fetch'], settings: { token: 'a-secret' },
|
||||
}),
|
||||
mockPlugin({ id: 'admins', managed: true, enabled: true, status: 'running', settings: { mode: 'dark' } }),
|
||||
],
|
||||
});
|
||||
|
||||
purgeSignedOutData();
|
||||
window.dispatchEvent(new Event('pagehide'));
|
||||
|
||||
const [mine, admins] = usePluginStore.getState().plugins;
|
||||
expect(mine).toMatchObject({ id: 'mine', enabled: false, settings: {} });
|
||||
expect(mine.grantedPermissions).toBeUndefined();
|
||||
expect(deactivatePlugin).toHaveBeenCalledWith('mine');
|
||||
expect(admins).toMatchObject({ id: 'admins', enabled: true, status: 'running', settings: { mode: 'dark' } });
|
||||
const stored = JSON.parse(localStorage.getItem('plugin-storage')!).state.plugins;
|
||||
expect(stored[0]).toMatchObject({ id: 'mine', enabled: false, settings: {} });
|
||||
expect(stored[0].grantedPermissions).toBeUndefined();
|
||||
expect(pluginStorage.deleteCode).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saving the plugin list', () => {
|
||||
const writes = (setItem: { mock: { calls: unknown[][] } }) =>
|
||||
setItem.mock.calls.filter(([key]) => key === 'plugin-storage') as [string, string][];
|
||||
|
||||
@@ -28,6 +28,14 @@ const SIGNED_OUT_STORAGE_KEYS = [
|
||||
|
||||
const SIGNED_OUT_SIGNAL = 'bulwark:signed-out';
|
||||
|
||||
// Stores the auth store can't import (they import it) clear themselves here.
|
||||
const signedOutCleanups = new Set<() => void>();
|
||||
|
||||
/** Run `cleanup` when everyone signs out in this tab. */
|
||||
export function onFullSignOut(cleanup: () => void): void {
|
||||
signedOutCleanups.add(cleanup);
|
||||
}
|
||||
|
||||
/** Remove the accounts' leftovers from browser storage. */
|
||||
export function purgeSignedOutData(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -40,6 +48,13 @@ export function purgeSignedOutData(): void {
|
||||
}
|
||||
// Attachment bytes a plugin staged for an upload that never started.
|
||||
void fileStorage.clearFiles().catch(() => {});
|
||||
for (const cleanup of signedOutCleanups) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch {
|
||||
/* one store failing must not keep the others' data */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Tell the other tabs of this browser that everyone signed out. */
|
||||
|
||||
@@ -26,6 +26,7 @@ import { fetchServerPluginList } from '@/lib/server-plugin-list';
|
||||
import { IMPLICIT_PERMISSIONS } from '@/lib/plugin-types';
|
||||
import type { Permission } from '@/lib/plugin-types';
|
||||
import { IS_LITE } from '@/lib/lite';
|
||||
import { onFullSignOut } from '@/lib/sign-out-cleanup';
|
||||
|
||||
let pluginInitializationPromise: Promise<void> | null = null;
|
||||
// One-time guard so we attach the locale and regional subscriptions only once.
|
||||
@@ -819,3 +820,21 @@ usePluginStore.subscribe((state, prev) => {
|
||||
forEachInstance(plugin.id, (instance) => instance.setSettings(next));
|
||||
}
|
||||
});
|
||||
|
||||
// A plugin the user installed runs on that user's consent and settings. Once
|
||||
// everyone has signed out, the next person at this browser must be asked
|
||||
// again: the code stays, the grants and settings go, and it is left off.
|
||||
// The admin's plugins are the admin's choice and stay as they are.
|
||||
onFullSignOut(() => {
|
||||
const { plugins } = usePluginStore.getState();
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin.managed && plugin.enabled) deactivatePlugin(plugin.id);
|
||||
}
|
||||
usePluginStore.setState({
|
||||
plugins: plugins.map((plugin) => {
|
||||
if (plugin.managed) return plugin;
|
||||
const { grantedPermissions: _granted, ...rest } = plugin;
|
||||
return { ...rest, enabled: false, status: 'disabled' as PluginStatus, error: undefined, settings: {} };
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user