Last of the five stacked Administration PRs. This one targets feat/admin-roles (#363). Merge order: #360 → #361 → #362 → #363 → this.
What it adds: Tenants under Access, gated by sysTenantQuery/sysTenantGet.
List: logo, storage used against its limit, and account limit.
Edition notice: a server that doesn't report enterprise gets a warning that anyone inside a tenant has only a normal user's permissions. It's a notice, not a gate; the section is still shown by permissions.
Panel
Name and logo. The logo can be https (drawn through the image proxy, since img-src is self/data/blob) or an image data URL; anything else is kept but not drawn.
Limits: accounts, groups, lists, domains, roles, DKIM keys, storage. Each is one quotas/<name> pointer, so the four not offered (OAuth clients, DNS servers, directories, ACME providers) keep their values. Empty means no limit.
Role: the most anyone inside can be allowed. Only grantable roles are offered.
What it holds: accounts, groups, lists, domains and roles, counted with memberTenantId queries and shown against the limits. A count that can't be read is left out, not shown as zero.
Domains: add a domain in no tenant, or take one out (memberTenantId on the domain). Accounts on it aren't moved.
Delete: offered only once every count reads zero, with type-the-name confirmation.
Accounts: a Tenant choice on create and edit, for admins who can read tenants. This is how a tenant gets its first administrator. You can't move your own account.
Mock
Tenant "Acme Corp" holding old-brand.example and an Administrator, plus spare.example to assign.
memberTenantId filters on Account, Domain, MailingList and Role queries.
x:Tenant get/query/set with quota validation and objectIsLinked.
Stalwart's rule: only an admin outside every tenant may set memberTenantId (a tenant admin gets "Cannot modify memberTenantId property").
Bug fixed: clearing a top-level property with null used to delete it from the working copy, and merging back kept the old value, so nothing had ever been cleared that way. Found by the domain move-out test.
Not tried live: prod has no tenants. KNOWN-ISSUES records what's read from source (including can_set_tenant in set.rs). The one assumption is that deleting a tenant that still holds something is refused; the panel avoids that case.
Translation: 39 new strings and 1 plural in all nine catalogues (1609/1625 each, 16 falling back as before).
Checked
Typecheck, 1284 web and 224 server tests pass, as do build and i18n:check.
New tests cover:
limit pointers, including leaving un-offered quotas alone;
per-kind counts, and domain moves;
which logos are drawn;
mock filters, linked delete, create/move/delete, and tenant-admin refusal;
panel: counts vs limits, delete blocked until empty, limit save, domain add, no domain moves without sysDomainUpdate.
In Chrome against the mock:
Acme Corp shows accounts 1 of 25, domains 1 of 2, and 2.1 GB of 50 GB, with delete blocked;
added spare.example (2 of 2), then took it out (1 of 2);
Wile's account shows Tenant: Acme Corp, and your own account's choice is locked;
created "Globex" (0 of 5), then deleted it through the dialog;
Last of the five stacked Administration PRs. This one targets `feat/admin-roles` (#363). Merge order: #360 → #361 → #362 → #363 → this.
**What it adds:** Tenants under Access, gated by `sysTenantQuery`/`sysTenantGet`.
- **List:** logo, storage used against its limit, and account limit.
- **Edition notice:** a server that doesn't report `enterprise` gets a warning that anyone inside a tenant has only a normal user's permissions. It's a notice, not a gate; the section is still shown by permissions.
**Panel**
- **Name and logo.** The logo can be https (drawn through the image proxy, since `img-src` is self/data/blob) or an image data URL; anything else is kept but not drawn.
- **Limits:** accounts, groups, lists, domains, roles, DKIM keys, storage. Each is one `quotas/<name>` pointer, so the four not offered (OAuth clients, DNS servers, directories, ACME providers) keep their values. Empty means no limit.
- **Role:** the most anyone inside can be allowed. Only grantable roles are offered.
- **What it holds:** accounts, groups, lists, domains and roles, counted with `memberTenantId` queries and shown against the limits. A count that can't be read is left out, not shown as zero.
- **Domains:** add a domain in no tenant, or take one out (`memberTenantId` on the domain). Accounts on it aren't moved.
- **Delete:** offered only once every count reads zero, with type-the-name confirmation.
**Accounts:** a Tenant choice on create and edit, for admins who can read tenants. This is how a tenant gets its first administrator. You can't move your own account.
**Mock**
- Tenant "Acme Corp" holding `old-brand.example` and an Administrator, plus `spare.example` to assign.
- `memberTenantId` filters on Account, Domain, MailingList and Role queries.
- `x:Tenant` get/query/set with quota validation and `objectIsLinked`.
- Stalwart's rule: only an admin outside every tenant may set `memberTenantId` (a tenant admin gets "Cannot modify memberTenantId property").
- **Bug fixed:** clearing a top-level property with `null` used to delete it from the working copy, and merging back kept the old value, so nothing had ever been cleared that way. Found by the domain move-out test.
**Not tried live:** prod has no tenants. KNOWN-ISSUES records what's read from source (including `can_set_tenant` in `set.rs`). The one assumption is that deleting a tenant that still holds something is refused; the panel avoids that case.
**Translation:** 39 new strings and 1 plural in all nine catalogues (1609/1625 each, 16 falling back as before).
**Checked**
- Typecheck, 1284 web and 224 server tests pass, as do build and i18n:check.
- New tests cover:
- limit pointers, including leaving un-offered quotas alone;
- per-kind counts, and domain moves;
- which logos are drawn;
- mock filters, linked delete, create/move/delete, and tenant-admin refusal;
- panel: counts vs limits, delete blocked until empty, limit save, domain add, no domain moves without `sysDomainUpdate`.
- In Chrome against the mock:
- Acme Corp shows accounts 1 of 25, domains 1 of 2, and 2.1 GB of 50 GB, with delete blocked;
- added `spare.example` (2 of 2), then took it out (1 of 2);
- Wile's account shows Tenant: Acme Corp, and your own account's choice is locked;
- created "Globex" (0 of 5), then deleted it through the dialog;
- Enterprise notice shown for the mock's "oss";
- no console errors.
**Merged** 2026-09-15 as coffey-labs/ihasmail@53d9cd37fbba
<sub>Rebuilt from: 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.
Last of the five stacked Administration PRs. This one targets
feat/admin-roles(#363). Merge order: #360 → #361 → #362 → #363 → this.What it adds: Tenants under Access, gated by
sysTenantQuery/sysTenantGet.enterprisegets a warning that anyone inside a tenant has only a normal user's permissions. It's a notice, not a gate; the section is still shown by permissions.Panel
img-srcis self/data/blob) or an image data URL; anything else is kept but not drawn.quotas/<name>pointer, so the four not offered (OAuth clients, DNS servers, directories, ACME providers) keep their values. Empty means no limit.memberTenantIdqueries and shown against the limits. A count that can't be read is left out, not shown as zero.memberTenantIdon the domain). Accounts on it aren't moved.Accounts: a Tenant choice on create and edit, for admins who can read tenants. This is how a tenant gets its first administrator. You can't move your own account.
Mock
old-brand.exampleand an Administrator, plusspare.exampleto assign.memberTenantIdfilters on Account, Domain, MailingList and Role queries.x:Tenantget/query/set with quota validation andobjectIsLinked.memberTenantId(a tenant admin gets "Cannot modify memberTenantId property").nullused to delete it from the working copy, and merging back kept the old value, so nothing had ever been cleared that way. Found by the domain move-out test.Not tried live: prod has no tenants. KNOWN-ISSUES records what's read from source (including
can_set_tenantinset.rs). The one assumption is that deleting a tenant that still holds something is refused; the panel avoids that case.Translation: 39 new strings and 1 plural in all nine catalogues (1609/1625 each, 16 falling back as before).
Checked
sysDomainUpdate.spare.example(2 of 2), then took it out (1 of 2);Merged 2026-09-15 as coffey-labs/ihasmail@53d9cd37fb
Rebuilt from: git history, session transcript.