Let an operator turn administration off

ADMINISTRATION=0 at launch removes in-app administration for everyone. The
account's permissions are no longer sent to the browser, so the menu never
appears, and the JMAP proxy refuses Stalwart registry methods other than the
account's own (settings, password, app passwords, API keys, public keys,
masked addresses). Hiding the menu alone would have left an administrator's
browser console able to make every call the menu made.

With administration on, the request body streams through untouched as before;
only an installation that turns it off reads and checks the body, forwarding
the parsed form so the server receives exactly what was inspected.
This commit is contained in:
2026-09-13 15:27:05 -07:00
parent 82e217155b
commit d279fe8f90
8 changed files with 149 additions and 3 deletions
+40
View File
@@ -0,0 +1,40 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { gateAdministration } from "./adminGate.js";
const req = (...methods: string[]) => JSON.stringify({ using: ["urn:ietf:params:jmap:core"], methodCalls: methods.map((m, i) => [m, {}, `c${i}`]) });
/**
* With ADMINISTRATION=0 an administrator's browser must not be a way round the
* operator's decision. Hiding the menu would leave the proxy forwarding the
* very calls the menu made.
*/
test("mail, calendars and the rest pass untouched", () => {
const r = gateAdministration(req("Email/query", "Mailbox/get", "CalendarEvent/set", "FileNode/get", "Principal/getAvailability"));
assert.equal(r.ok, true);
});
test("the account's own registry objects pass", () => {
assert.equal(gateAdministration(req("x:AccountSettings/get", "x:AppPassword/set", "x:PublicKey/get", "x:MaskedEmail/set")).ok, true);
});
test("directory and server objects are refused, and named", () => {
for (const m of ["x:Account/get", "x:Domain/set", "x:Role/query", "x:Tenant/get", "x:SystemSettings/set", "x:DkimSignature/get"]) {
assert.deepEqual(gateAdministration(req("Email/get", m)), { ok: false, method: m });
}
});
test("a body that cannot be read is refused rather than forwarded unchecked", () => {
assert.deepEqual(gateAdministration("{not json"), { ok: false, method: null });
assert.deepEqual(gateAdministration(JSON.stringify({ methodCalls: "x:Account/get" })), { ok: false, method: null });
assert.deepEqual(gateAdministration(JSON.stringify({ methodCalls: [[{}, {}, "c"]] })), { ok: false, method: null });
});
test("what is forwarded is what was checked", () => {
// A duplicate key is read one way by JSON.parse; forwarding the parsed form
// means the server cannot read it the other way.
const raw = '{"methodCalls":[["x:Account/get",{},"a"]],"methodCalls":[["Email/get",{},"b"]]}';
const r = gateAdministration(raw);
assert.equal(r.ok, true);
if (r.ok) assert.equal(r.body, JSON.stringify({ methodCalls: [["Email/get", {}, "b"]] }));
});