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;
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
permissions.rs(enabled and disabled unioned across the tree, then subtracted).enabledPermissions/<p>,disabledPermissions/<p>,roleIds/<id>).Server: new
GET /api/admin/permissions./api/schemaas the signed-in account and returns only{name, label}pairs.ADMINISTRATIONand own-device gates as the registry proxy.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.source.jsonis the 0.16.22 snapshot. A test holds each language to it: nothing missing, nothing stale, headings exact.Mock
x:Role/setwith the grant check, loop refusal, andobjectIsLinkedfor roles used by accounts, other roles, or the defaults.x:Authentication/get./api/schemaserved gzipped from the snapshot.Not tried live: role writes, and
/api/schemafetched through ihasmail's server. Both come from source and schema, and are recorded in KNOWN-ISSUES. Thex:Roleshape 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
sysLog*; allowedsysDkimSignatureCreate, denied the inheritedjmapEmailGet; saved and reloaded, still there;Merged 2026-09-15 as coffey-labs/ihasmail@5f28393039
Rebuilt from: GH Archive, git history, session transcript.