fix(tenants): name a personal tenant after its owner #503

Merged
longjacksonle merged 3 commits from fix/PMS-743-personal-tenant-name into main 2026-08-08 06:17:16 +02:00

Closes PMS-743.

Problem

Every auto-provisioned tenant was named the constant "My workspace" (src/modules/tenants/service.rs:263), and that name goes to external recipients: the client request-form email subject ({{form_name}} request form from {{tenant_name}}) and the invitation email.

Staging had eight tenants sharing that name, one per person who has ever signed in, so eight MSPs' clients would receive mail from "My workspace" until each owner intervened by hand. Found while verifying MAPPS-425, where a real client email arrived reading longle request form from My workspace.

The constant was not wrong when personal tenants landed. The slug is uuid-derived precisely so provisioning needs no human name, and nothing external rendered the display name then. Both emailed surfaces arrived later.

What this does

ensure_personal_tenant now takes the IdP's given_name and email. The placement path already held both (middleware.rs:654-663) and passed neither. Preference order:

  1. the given name, a real name the owner chose
  2. a first name synthesised from a real email address
  3. the old constant

Step 2 reuses synthetic_name_from_email rather than reimplementing its rules. That helper already refuses to derive a name from mokosh's own @unresolved.invalid JIT placeholder, from a UUID local-part, or from a local-part with no usable segments, and those are exactly the cases that must not become a tenant name. It is now pub(crate) alongside its fallback pair, so a caller can tell "derived from the user" from "gave up".

Falling back to the generic is deliberate rather than clever: "Mokosh User's workspace" or "7fa2b249's workspace" reads worse to a client than the honest generic, and the owner can rename it in Settings either way now that mokosh-apps #493 has merged.

The derived name is truncated to the VARCHAR(255) column width instead of being allowed to fail the insert. Provisioning runs on someone's very first login; a long name must not be what breaks it.

The possessive is always 's, including after a trailing s ("Chris's workspace"). Common style, and no branching on spelling.

Not done, on purpose

Existing "My workspace" rows are untouched. Renaming a tenant changes the phrase its owner must type to confirm a data import, so a backfill should be an explicit decision rather than a side effect of a deploy. Owners rename through Settings.

Naming from the bunyip org instead of the owner was considered and rejected here: it would deepen the existing split where the org switcher shows bunyip's name while emails use mokosh's tenants.name. Which system owns an organisation's name is still an open product question, noted in the ticket.

Verification

Full integration suite: 750 passed, 0 failed. just check clean.

Six unit tests cover the rule itself (given name wins, blank treated as absent, unusable owner keeps the generic, truncation, trailing-s possessive), plus one integration test that provisions through the real path and reads the row back, so a change to the INSERT cannot quietly restore the constant while the pure function still looks correct.

One caveat worth stating: an earlier full-suite run reported a single failure that I did not capture the name of, and that did not reproduce across two subsequent full runs. I could not identify it, so I am flagging it rather than claiming a clean history.

Closes PMS-743. ## Problem Every auto-provisioned tenant was named the constant "My workspace" (`src/modules/tenants/service.rs:263`), and that name goes to external recipients: the client request-form email subject (`{{form_name}} request form from {{tenant_name}}`) and the invitation email. Staging had eight tenants sharing that name, one per person who has ever signed in, so eight MSPs' clients would receive mail from "My workspace" until each owner intervened by hand. Found while verifying MAPPS-425, where a real client email arrived reading `longle request form from My workspace`. The constant was not wrong when personal tenants landed. The slug is uuid-derived precisely so provisioning needs no human name, and nothing external rendered the display name then. Both emailed surfaces arrived later. ## What this does `ensure_personal_tenant` now takes the IdP's `given_name` and `email`. The placement path already held both (`middleware.rs:654-663`) and passed neither. Preference order: 1. the given name, a real name the owner chose 2. a first name synthesised from a real email address 3. the old constant Step 2 reuses `synthetic_name_from_email` rather than reimplementing its rules. That helper already refuses to derive a name from mokosh's own `@unresolved.invalid` JIT placeholder, from a UUID local-part, or from a local-part with no usable segments, and those are exactly the cases that must not become a tenant name. It is now `pub(crate)` alongside its fallback pair, so a caller can tell "derived from the user" from "gave up". Falling back to the generic is deliberate rather than clever: "Mokosh User's workspace" or "7fa2b249's workspace" reads worse to a client than the honest generic, and the owner can rename it in Settings either way now that mokosh-apps #493 has merged. The derived name is truncated to the `VARCHAR(255)` column width instead of being allowed to fail the insert. Provisioning runs on someone's very first login; a long name must not be what breaks it. The possessive is always `'s`, including after a trailing s ("Chris's workspace"). Common style, and no branching on spelling. ## Not done, on purpose Existing "My workspace" rows are untouched. Renaming a tenant changes the phrase its owner must type to confirm a data import, so a backfill should be an explicit decision rather than a side effect of a deploy. Owners rename through Settings. Naming from the bunyip org instead of the owner was considered and rejected here: it would deepen the existing split where the org switcher shows bunyip's name while emails use mokosh's `tenants.name`. Which system owns an organisation's name is still an open product question, noted in the ticket. ## Verification Full integration suite: 750 passed, 0 failed. `just check` clean. Six unit tests cover the rule itself (given name wins, blank treated as absent, unusable owner keeps the generic, truncation, trailing-s possessive), plus one integration test that provisions through the real path and reads the row back, so a change to the INSERT cannot quietly restore the constant while the pure function still looks correct. One caveat worth stating: an earlier full-suite run reported a single failure that I did not capture the name of, and that did not reproduce across two subsequent full runs. I could not identify it, so I am flagging it rather than claiming a clean history.
`synthetic_name_from_email` already knows how to refuse a name it should not derive: mokosh's own `@unresolved.invalid` JIT placeholder, a UUID local-part, a local-part with no usable segments. PMS-743 needs exactly those rules when naming a tenant, and a second copy would be a second thing to keep correct.

Made `pub(crate)` alongside its fallback pair, which callers need in order to tell "derived from the user" from "gave up".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011myibMMwyb6za3GVWJGkiX
PMS-743: every auto-provisioned tenant was named the constant "My workspace", and that name is customer-facing. It renders in the client request-form email subject and in invitation mail, so staging had eight tenants sharing one name and eight MSPs whose clients received mail from "My workspace" until somebody intervened by hand.

The constant was not wrong when personal tenants landed: the slug is uuid-derived precisely so provisioning needs no human name, and nothing external rendered the display name yet. Both emailed surfaces arrived later.

`ensure_personal_tenant` now takes the IdP's `given_name` and `email`, which the placement path already held and passed nothing of. Preference order is the given name, then a first name synthesised from a real email, then the old constant. Falling back is deliberate: "Mokosh User's workspace" or "7fa2b249's workspace" reads worse to a client than the honest generic, and the owner renames it in Settings either way (MAPPS-426).

The name is truncated to the VARCHAR(255) column width rather than allowed to fail the insert. Provisioning runs on someone's first login; a long name must not be the thing that breaks it.

Existing rows are untouched. Renaming a tenant changes what its owner must type to confirm a data import, so a backfill is an explicit decision, not a side effect of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011myibMMwyb6za3GVWJGkiX
test(tenants): pin the provisioned tenant's name to its owner
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m44s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m0s
Create release / Gate (release-branch merges only) (pull_request) Successful in 1s
Create release / Create release from merged PR (pull_request) Has been skipped
Integration / integration tests (pull_request) Successful in 9m4s
f40d80da8f
Unit tests cover the naming rule itself (given name wins, blank is absent, an unusable owner keeps the generic, truncation, the trailing-s possessive). This one goes through the real provisioning path and reads the row back, so a future change to the INSERT cannot quietly restore the constant while the pure function still looks right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011myibMMwyb6za3GVWJGkiX
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-08 06:14:20 +02:00
longjacksonle deleted branch fix/PMS-743-personal-tenant-name 2026-08-08 06:17:16 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!503
No description provided.