mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
120 lines
5.1 KiB
TypeScript
120 lines
5.1 KiB
TypeScript
/**
|
|
* The OS policies as a page sees them, through the desktop app's `policy` bridge.
|
|
* In a browser, or under a shell that predates this, there's no policy: everything
|
|
* here says "allowed" and "not locked", so browsers behave as they always did.
|
|
*
|
|
* Features ask here rather than reading the bridge themselves.
|
|
*/
|
|
|
|
import { useSyncExternalStore } from 'react';
|
|
import { getDesktopBridge } from './bridge';
|
|
import type { DesktopPolicyBridge } from './types';
|
|
import {
|
|
DEFAULT_POLICY_SNAPSHOT,
|
|
effectivePolicyValue,
|
|
jmapServerAllowed,
|
|
pluginAllowedByPolicy,
|
|
type PolicyEntry,
|
|
type PolicyKey,
|
|
type PolicySnapshot,
|
|
} from './policy-schema';
|
|
|
|
/** A plugin the device's policy refuses, on a plugin's record and in the settings page. */
|
|
export const PLUGIN_POLICY_REFUSED_ERROR = 'device-policy';
|
|
|
|
let cached: { bridge: DesktopPolicyBridge; snapshot: PolicySnapshot } | null = null;
|
|
|
|
/**
|
|
* The same object until the policy changes. The bridge can't give that: contextBridge
|
|
* copies what it returns, so each `get()` is a new object, and useSyncExternalStore takes
|
|
* a new object for a change and renders forever.
|
|
*/
|
|
export function getDesktopPolicy(): PolicySnapshot {
|
|
const bridge = getDesktopBridge()?.policy;
|
|
if (!bridge?.get) return DEFAULT_POLICY_SNAPSHOT;
|
|
if (cached?.bridge !== bridge) {
|
|
const entry = { bridge, snapshot: bridge.get() };
|
|
cached = entry;
|
|
bridge.onChange?.((snapshot) => {
|
|
entry.snapshot = snapshot;
|
|
});
|
|
}
|
|
return cached.snapshot;
|
|
}
|
|
|
|
export function onDesktopPolicyChange(listener: (snapshot: PolicySnapshot) => void): () => void {
|
|
// First, so what the listener reads is already the new value.
|
|
getDesktopPolicy();
|
|
return getDesktopBridge()?.policy?.onChange?.(listener) ?? (() => {});
|
|
}
|
|
|
|
const serverSnapshot = () => DEFAULT_POLICY_SNAPSHOT;
|
|
|
|
/** The policies, live, for a component. */
|
|
export function useDesktopPolicy(): PolicySnapshot {
|
|
return useSyncExternalStore(onDesktopPolicyChange, getDesktopPolicy, serverSnapshot);
|
|
}
|
|
|
|
/** Policy has the OS certificate store off: by its own switch, or with all plugins. */
|
|
export function keystoreOffByDesktopPolicy(): boolean {
|
|
const policy = getDesktopPolicy();
|
|
return !policy.KeystoreEnabled.value || policy.PluginsMode.value === 'off';
|
|
}
|
|
|
|
export function desktopPolicyEntry<K extends PolicyKey>(key: K): PolicyEntry<K> {
|
|
return getDesktopPolicy()[key];
|
|
}
|
|
|
|
/** Forced beats the user's choice, which beats a recommendation. See policy-schema.ts. */
|
|
export function desktopPolicyValue<K extends PolicyKey>(key: K, userChoice?: PolicySnapshot[K]['value'] | null): PolicySnapshot[K]['value'] {
|
|
return effectivePolicyValue(desktopPolicyEntry(key), userChoice) as PolicySnapshot[K]['value'];
|
|
}
|
|
|
|
export function pluginAllowedByDesktopPolicy(id: string, snapshot: PolicySnapshot = getDesktopPolicy()): boolean {
|
|
return pluginAllowedByPolicy(snapshot.PluginsMode.value, snapshot.PluginAllowlist.value, id);
|
|
}
|
|
|
|
/**
|
|
* Whether a plugin may run. A package names its own id, so with a list only what an
|
|
* admin deployed (`managed`) can match it: anyone could build a package called the
|
|
* same as a listed plugin.
|
|
*/
|
|
export function pluginMayRunByDesktopPolicy(plugin: { id: string; managed?: boolean }, snapshot: PolicySnapshot = getDesktopPolicy()): boolean {
|
|
if (snapshot.PluginsMode.value === 'allowed') return true;
|
|
return plugin.managed === true && pluginAllowedByDesktopPolicy(plugin.id, snapshot);
|
|
}
|
|
|
|
/** With a list or none, nobody adds a package of their own. Same reason. */
|
|
export function pluginUploadsAllowedByDesktopPolicy(snapshot: PolicySnapshot = getDesktopPolicy()): boolean {
|
|
return snapshot.PluginsMode.value === 'allowed';
|
|
}
|
|
|
|
export function jmapServerAllowedByDesktopPolicy(serverUrl: string, snapshot: PolicySnapshot = getDesktopPolicy()): boolean {
|
|
return jmapServerAllowed(snapshot.AllowedJmapServers.value, serverUrl);
|
|
}
|
|
|
|
/**
|
|
* Whether the login page may offer a server of the user's own. A forced Bulwark
|
|
* server decides that alone, unless the same policy leaves direct connections on.
|
|
* `AllowStandalone` off says the same.
|
|
*/
|
|
export function customServerAllowedByDesktopPolicy(snapshot: PolicySnapshot = getDesktopPolicy()): boolean {
|
|
const { AllowStandalone, InstanceUrl } = snapshot;
|
|
if (AllowStandalone.locked) return AllowStandalone.value;
|
|
return !InstanceUrl.locked;
|
|
}
|
|
|
|
/**
|
|
* Whether accounts may keep mail on this device: `forced` when the policy says so
|
|
* either way, `fallback` for the default where the user hasn't chosen. Forced off
|
|
* feeds `noOffline` (lib/device-policy.ts), which also clears what's there already.
|
|
* `invalid`: forced to something that makes no sense. Nothing new is kept, and a typo
|
|
* deletes nothing.
|
|
*/
|
|
export function desktopOfflinePolicy(snapshot: PolicySnapshot = getDesktopPolicy()): { forced: boolean | null; fallback: boolean | null; invalid?: true } {
|
|
const { OfflineEnabled } = snapshot;
|
|
if (OfflineEnabled.locked && OfflineEnabled.invalid) return { forced: false, fallback: null, invalid: true };
|
|
if (OfflineEnabled.locked) return { forced: OfflineEnabled.value, fallback: null };
|
|
return { forced: null, fallback: OfflineEnabled.source === 'default' ? null : OfflineEnabled.value };
|
|
}
|