Offer administration only on a device marked as your own
A session signed in without "This is my own device" can no longer administer. The server withholds the account's permissions from it and the JMAP proxy refuses registry methods beyond the account's own, the same gate ADMINISTRATION=0 uses. A borrowed or shared machine is where nobody should be able to reset a password or remove a domain. An administrator in such a session still sees Administration in the account menu, greyed out, with the reason and the fix: sign in again with the box ticked. The server tells that session only that the account administers. The gate now reads the body only when it could name a registry method -- "x: in the text, or a \u escape that could spell one -- so ordinary mail traffic from an untrusted session is forwarded untouched. 1 new string, translated in all nine catalogues, quoting each language's own label for the tickbox; strings falling back to English stay at 16.
This commit is contained in:
+40
-2
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* What the JMAP proxy lets through when an operator has turned in-app
|
||||
* administration off (`ADMINISTRATION=0`).
|
||||
* What the JMAP proxy lets through for a session that may not administer:
|
||||
* the operator turned it off (`ADMINISTRATION=0`), or the session was signed
|
||||
* in without "This is my own device".
|
||||
*
|
||||
* Hiding the menu is not turning it off. `/api/jmap` forwards any method the
|
||||
* browser sends, and Stalwart's registry answers whatever the credential's role
|
||||
@@ -21,6 +22,42 @@ const SELF_SERVICE = new Set(["AccountSettings", "AccountPassword", "AppPassword
|
||||
|
||||
export type GateResult = { ok: true; body: string } | { ok: false; method: string | null };
|
||||
|
||||
/**
|
||||
* Whether a session may administer at all: the installation allows it, and
|
||||
* the person signing in said the device is their own.
|
||||
*
|
||||
* The second half is the operator's rule, not Stalwart's. A borrowed laptop or
|
||||
* a library machine is exactly where a session should not be able to reset a
|
||||
* password or remove a domain, and "This is my own device" is the one thing
|
||||
* the sign-in form already asks that says where it is being used. An untrusted
|
||||
* session is also signed out when idle and wipes its local data, so nothing
|
||||
* about it suits an administrator's work.
|
||||
*/
|
||||
export function administrationAllowed(enabled: boolean, remember: boolean): boolean {
|
||||
return enabled && remember;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an account's permissions would put Administration in its menu --
|
||||
* the same test the client makes, so the server can say why it is missing
|
||||
* without handing over the permissions themselves.
|
||||
*/
|
||||
export function grantsAdministration(permissions: readonly string[]): boolean {
|
||||
const has = new Set(permissions);
|
||||
return (has.has("sysAccountQuery") && has.has("sysAccountGet")) || (has.has("sysDomainQuery") && has.has("sysDomainGet"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a body could hold a registry method name at all, so the common case
|
||||
* -- mail, calendars, contacts from a session that may not administer -- skips
|
||||
* the parse. A method name is a JSON string starting `x:`, which appears in the
|
||||
* text as `"x:` unless written with a `\u` escape; a body with neither cannot
|
||||
* contain one, and is forwarded exactly as it came.
|
||||
*/
|
||||
export function mayNameRegistryMethod(raw: string): boolean {
|
||||
return raw.includes('"x:') || raw.includes("\\u");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a JMAP request body. On success, hands back the body to forward --
|
||||
* serialised from what was inspected, so the server can never be sent
|
||||
@@ -28,6 +65,7 @@ export type GateResult = { ok: true; body: string } | { ok: false; method: strin
|
||||
* way here and another way there).
|
||||
*/
|
||||
export function gateAdministration(raw: string): GateResult {
|
||||
if (!mayNameRegistryMethod(raw)) return { ok: true, body: raw };
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
|
||||
Reference in New Issue
Block a user