Add Roles to Administration, with Stalwart's permissions in every language #363

Closed
opened 2026-09-15 16:13:29 +00:00 by jcoffey-dev · 0 comments
Owner

Fourth of the five stacked Administration PRs. This one targets feat/admin-lists (#362). The five are meant to merge together, in order.

What it adds: Roles, under a new Access heading, gated by sysRoleQuery/sysRoleGet.

List page: every role, with the permissions it grants once inheritance is followed, what it builds on, and a "Default for users/…" note on the roles Stalwart hands out by default (read from x:Authentication).

Panel

  • Name, and the roles it builds on. Loops, and bases carrying permissions the viewer lacks, are disabled.
  • Permission picker: Stalwart's full list, under its own 59 headings, collapsible, searchable, with a filter for all, granted or set here.
    • Each permission is Not set / Inherit, Allow or Deny.
    • Inherited ones say where they come from ("Granted by User", "Denied by …").
    • Denials anywhere in the tree win, which matches permissions.rs (enabled and disabled unioned across the tree, then subtracted).
    • Allow is disabled for anything the viewer doesn't hold, because Stalwart refuses it.
  • Save: only changed pointers (enabledPermissions/<p>, disabledPermissions/<p>, roleIds/<id>).
  • Guards:
    • a role carrying permissions the viewer lacks opens read-only with no delete, since Stalwart checks grants but not deletes;
    • a default role warns and can't be deleted;
    • an in-use role is refused by the server, and the error names what uses it.

Server: new GET /api/admin/permissions.

  • Fetches Stalwart's /api/schema as the signed-in account and returns only {name, label} pairs.
  • Handles a gzipped or already-inflated body.
  • Uses the same ADMINISTRATION and own-device gates as the registry proxy.
  • Cached in memory for an hour per server.

Translations: all 661 permission labels, per your call

  • web/src/locales/permissions/<lang>.ts, one per language: 661 labels and 59 headings, keyed by permission name, so Stalwart wording changes don't orphan them.
  • Loaded only when Roles opens (about 8 KB gzipped each). A permission a later Stalwart adds falls back to its English label.
  • source.json is the 0.16.22 snapshot. A test holds each language to it: nothing missing, nothing stale, headings exact.
  • Written by one translation pass per language against that catalogue's existing terminology. Not natively reviewed. Terms the translators flagged as least certain: principal, throttles, listeners, lookups, milters, masked emails, samples, schedules, email submission, MTA stage settings. These are listed in KNOWN-ISSUES.
  • Main catalogues: 52 new strings and 2 plurals in all nine (1569/1585 each, 16 falling back as before).

Mock

  • x:Role/set with the grant check, loop refusal, and objectIsLinked for roles used by accounts, other roles, or the defaults.
  • x:Authentication/get.
  • /api/schema served gzipped from the snapshot.
  • A fourth sample role with a denial.

Not tried live: role writes, and /api/schema fetched through ihasmail's server. Both come from source and schema, and are recorded in KNOWN-ISSUES. The x:Role shape and the four bootstrap roles were confirmed read-only on prod. I'd suggest a throwaway-role run on prod before merging, like groups.

Checked

  • Typecheck, 1276 web and 220 server tests pass, as do build and i18n:check.
  • New tests cover:
    • schema extraction (including gzip);
    • inheritance and denials, set patches, loops, and the outranking check;
    • label splitting and English fallback;
    • per-language coverage for all nine;
    • mock grant check, loops, in-use refusal and defaults;
    • panel: inherited note, saves only pointers, Allow locked for unheld permissions, outranking role read-only, default role guarded.
  • In Chrome against the mock:
    • list and counts (the auditor's 8 reflects an inherited denial);
    • Helpdesk: Allow disabled for all five sysLog*; allowed sysDkimSignatureCreate, denied the inherited jmapEmailGet; saved and reloaded, still there;
    • User shows the default warning and delete is blocked;
    • German interface shows the German labels ("Konten abrufen", "Nicht gesetzt / Erlauben / Verweigern");
    • created "Test role" on User (4 permissions), then deleted it through the dialog;
    • no console errors.

Merged 2026-09-15 as coffey-labs/ihasmail@5f28393039

Rebuilt from: GH Archive, git history, session transcript.

Fourth of the five stacked Administration PRs. This one targets `feat/admin-lists` (#362). The five are meant to merge together, in order. **What it adds:** Roles, under a new **Access** heading, gated by `sysRoleQuery`/`sysRoleGet`. **List page:** every role, with the permissions it grants once inheritance is followed, what it builds on, and a "Default for users/…" note on the roles Stalwart hands out by default (read from `x:Authentication`). **Panel** - **Name, and the roles it builds on.** Loops, and bases carrying permissions the viewer lacks, are disabled. - **Permission picker:** Stalwart's full list, under its own 59 headings, collapsible, searchable, with a filter for all, granted or set here. - Each permission is *Not set / Inherit*, *Allow* or *Deny*. - Inherited ones say where they come from ("Granted by User", "Denied by …"). - Denials anywhere in the tree win, which matches `permissions.rs` (enabled and disabled unioned across the tree, then subtracted). - *Allow* is disabled for anything the viewer doesn't hold, because Stalwart refuses it. - **Save:** only changed pointers (`enabledPermissions/<p>`, `disabledPermissions/<p>`, `roleIds/<id>`). - **Guards:** - a role carrying permissions the viewer lacks opens read-only with no delete, since Stalwart checks grants but not deletes; - a default role warns and can't be deleted; - an in-use role is refused by the server, and the error names what uses it. **Server:** new `GET /api/admin/permissions`. - Fetches Stalwart's `/api/schema` as the signed-in account and returns only `{name, label}` pairs. - Handles a gzipped or already-inflated body. - Uses the same `ADMINISTRATION` and own-device gates as the registry proxy. - Cached in memory for an hour per server. **Translations: all 661 permission labels, per your call** - `web/src/locales/permissions/<lang>.ts`, one per language: 661 labels and 59 headings, keyed by permission name, so Stalwart wording changes don't orphan them. - Loaded only when Roles opens (about 8 KB gzipped each). A permission a later Stalwart adds falls back to its English label. - `source.json` is the 0.16.22 snapshot. A test holds each language to it: nothing missing, nothing stale, headings exact. - Written by one translation pass per language against that catalogue's existing terminology. Not natively reviewed. Terms the translators flagged as least certain: *principal*, *throttles*, *listeners*, *lookups*, *milters*, *masked emails*, *samples*, *schedules*, *email submission*, MTA stage settings. These are listed in KNOWN-ISSUES. - Main catalogues: 52 new strings and 2 plurals in all nine (1569/1585 each, 16 falling back as before). **Mock** - `x:Role/set` with the grant check, loop refusal, and `objectIsLinked` for roles used by accounts, other roles, or the defaults. - `x:Authentication/get`. - `/api/schema` served gzipped from the snapshot. - A fourth sample role with a denial. **Not tried live:** role writes, and `/api/schema` fetched through ihasmail's server. Both come from source and schema, and are recorded in KNOWN-ISSUES. The `x:Role` shape and the four bootstrap roles were confirmed read-only on prod. I'd suggest a throwaway-role run on prod before merging, like groups. **Checked** - Typecheck, 1276 web and 220 server tests pass, as do build and i18n:check. - New tests cover: - schema extraction (including gzip); - inheritance and denials, set patches, loops, and the outranking check; - label splitting and English fallback; - per-language coverage for all nine; - mock grant check, loops, in-use refusal and defaults; - panel: inherited note, saves only pointers, *Allow* locked for unheld permissions, outranking role read-only, default role guarded. - In Chrome against the mock: - list and counts (the auditor's 8 reflects an inherited denial); - Helpdesk: *Allow* disabled for all five `sysLog*`; allowed `sysDkimSignatureCreate`, denied the inherited `jmapEmailGet`; saved and reloaded, still there; - User shows the default warning and delete is blocked; - German interface shows the German labels ("Konten abrufen", "Nicht gesetzt / Erlauben / Verweigern"); - created "Test role" on User (4 permissions), then deleted it through the dialog; - no console errors. **Merged** 2026-09-15 as coffey-labs/ihasmail@5f28393039a4 <sub>Rebuilt from: GH Archive, git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.