From ce5eb04c2d6525a5a95460bbfb68389b11fa660a Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 15 Sep 2026 09:31:10 -0700 Subject: [PATCH] Show only the Enterprise notice on Tenants when the server is not Enterprise On a server that does not report Enterprise -- or reports no edition -- tenants hold nobody to anything beyond an ordinary user's permissions, so the page is the notice alone: no New tenant, no search, no list, and no tenant query is made. The mock's edition is MOCK_EDITION now (default oss, as before), so MOCK_EDITION=enterprise brings the section back to work on. --- FEATURES.md | 11 ++-- README.md | 5 +- server/src/mock/index.ts | 4 +- web/src/views/admin/TenantsAdmin.tsx | 31 ++++++--- .../admin/__tests__/tenants-admin.test.tsx | 65 +++++++++++++++++++ 5 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 web/src/views/admin/__tests__/tenants-admin.test.tsx diff --git a/FEATURES.md b/FEATURES.md index 1dbd1d1..5b15389 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1264,9 +1264,11 @@ Stalwart shows the server's English until it is translated. A tenant is a separate organisation on the same server — its own people, domains and limits, and an administrator who manages only what is in it. It is -a Stalwart Enterprise feature: on a server that does not report Enterprise the -page says that anyone inside a tenant has only an ordinary user's permissions. -For a role with `sysTenantQuery` and `sysTenantGet`, under Access: +a Stalwart Enterprise feature. On a server that does not report Enterprise — or +reports no edition at all — the page is only a notice that anyone inside a +tenant has only an ordinary user's permissions: no list, no search, nothing to +create. On Enterprise, for a role with `sysTenantQuery` and `sysTenantGet`, +under Access: - **List and search** tenants, with each one's storage and account limit. - **Create and edit** a tenant's name, logo (an https address, drawn through the @@ -1815,7 +1817,8 @@ demo user is: `admin` (the default), `tenant-admin` (the queue but not the history), `helpdesk` — a custom role that may view and edit accounts but not create or delete them, and read domains — or `user`, who is not offered the menu at all. `MOCK_METRICS=off` refuses the history the way a Community server -does. Two mailing lists round it out. +does, and `MOCK_EDITION=enterprise` reports Enterprise so Tenants can be +worked on (the default, `oss`, shows only its notice). Two mailing lists round it out. --- diff --git a/README.md b/README.md index 024e64a..3258ac2 100644 --- a/README.md +++ b/README.md @@ -403,13 +403,14 @@ without a real mailbox. It reproduces the things a naive fake would get wrong, because each cost a live debugging session: `urn:stalwart:jmap` advertised **per-account** rather than session-level, identity signatures capped at 2047 **bytes**, and `CalendarEvent/set` speaking Stalwart's vocabulary rather than -RFC 8984's. Five switches: `MOCK_NO_FUTURE_RELEASE=1` advertises FUTURERELEASE +RFC 8984's. Six switches: `MOCK_NO_FUTURE_RELEASE=1` advertises FUTURERELEASE and then drops every hold; `MOCK_NO_REGISTRY=1` omits the Stalwart capability so the sign-in refusal can be tested; and `MOCK_NO_SCHEDULING_SEND=1` refuses a calendar write that asks for scheduling messages, the way an account without that permission is refused; `MOCK_ROLE` decides who the demo user is for Administration — `admin` (the default), `tenant-admin`, `helpdesk` or `user`; -and `MOCK_METRICS=off` refuses the dashboard's metric history, as Community does. +`MOCK_METRICS=off` refuses the dashboard's metric history, as Community does; +and `MOCK_EDITION=enterprise` reports Enterprise, which Tenants needs. It tracks the current release rather than 0.16 in general, and each behaviour is confirmed against a real server before it is copied here — the comments say diff --git a/server/src/mock/index.ts b/server/src/mock/index.ts index 1f6fd8c..5e27929 100644 --- a/server/src/mock/index.ts +++ b/server/src/mock/index.ts @@ -45,6 +45,8 @@ const SHARED_CAPS: Obj = { const USER = process.env.MOCK_USER ?? "demo@example.com"; /** Locale the fake directory reports for the account (POSIX style, as Stalwart does). */ const MOCK_LOCALE = process.env.MOCK_LOCALE ?? "en_US"; +/** What /api/account reports. Tenants are managed only on "enterprise"; MOCK_EDITION=enterprise to develop them. */ +const MOCK_EDITION = process.env.MOCK_EDITION ?? "oss"; const PASS = process.env.MOCK_PASS ?? "demo"; /** * Credential state, mutable so the self-service flows can be exercised against @@ -1430,7 +1432,7 @@ export const server = createServer(async (req, res) => { // The account info endpoint; the only place a server reports its edition. if (url.pathname === "/api/account" && req.method === "GET") { res.writeHead(200, { "content-type": "application/json" }); - return res.end(JSON.stringify({ permissions: directory.permissions, edition: "oss", locale: MOCK_LOCALE })); + return res.end(JSON.stringify({ permissions: directory.permissions, edition: MOCK_EDITION, locale: MOCK_LOCALE })); } // The registry schema, cut down to the permission list the Roles picker // reads. Gzipped as the real file is, from the 0.16.22 snapshot the diff --git a/web/src/views/admin/TenantsAdmin.tsx b/web/src/views/admin/TenantsAdmin.tsx index 176f8b9..5919df6 100644 --- a/web/src/views/admin/TenantsAdmin.tsx +++ b/web/src/views/admin/TenantsAdmin.tsx @@ -18,14 +18,33 @@ const PAGE_SIZE = 50; * Tenants: separate organisations on one server, each with its own people, * domains and limits. * - * Shown to whoever may read them, whatever the edition says -- the edition is a - * licence claim, not an authority -- but on a server that does not report - * Enterprise the page says what that means for the people inside one. + * The section is offered to whoever may read tenants, but on a server that does + * not report Enterprise the page is only the notice: tenants there hold nobody + * to anything beyond an ordinary user's permissions, so there is nothing worth + * creating or listing. A server that reports no edition at all counts as not + * Enterprise. */ export function TenantsAdmin({ selectedId }: { selectedId?: string }) { + const edition = useSession((s) => s.session?.ihasmail?.server?.edition ?? null); + if (edition !== "enterprise") { + return ( +
+
+
+

{t("Tenants")}

+

{t("Separate organisations on one server, each with its own people, domains and limits.")}

+
+
+

{t("Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.")}

+
+ ); + } + return ; +} + +function EnterpriseTenants({ selectedId }: { selectedId?: string }) { const [, navigate] = useLocation(); const perms = usePermissions(); - const edition = useSession((s) => s.session?.ihasmail?.server?.edition ?? null); const [text, setText] = useState(""); const [query, setQuery] = useState(""); const [position, setPosition] = useState(0); @@ -100,10 +119,6 @@ export function TenantsAdmin({ selectedId }: { selectedId?: string }) { )} - {edition !== "enterprise" && ( -

{t("Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.")}

- )} -