Skip to content

Fix an existing user being renamed when the admin form rebuilds its changeset - #671

Merged
ddon merged 1 commit into
BeamLabEU:mainfrom
timujinne:fix/username-rewritten-on-edit
Jul 29, 2026
Merged

Fix an existing user being renamed when the admin form rebuilds its changeset#671
ddon merged 1 commit into
BeamLabEU:mainfrom
timujinne:fix/username-rewritten-on-edit

Conversation

@timujinne

Copy link
Copy Markdown
Contributor

The bug

Open a user in the admin form, click "change password", save — and the user is renamed. maria becomes maria_1; do it again and she becomes maria_2.

Why

Two things combine, both in User:

maybe_generate_username_from_email/1 runs on everyregistration_changeset/3, and generates a username whenever the params carry none. That is right for a new user, but the admin form rebuilds this changeset for an already-saved user several times while editing — reload_changeset_with_password/2 does it every time the password field is toggled — and those rebuilds send no username.

ensure_unique_username/2 then asked the database whether the generated name was taken, without excluding the user it was generating for. Maria's own row made maria look unavailable to Maria, so the walk moved on to maria_1.

The fix

  • Generation is skipped for a persisted user that already has a username. A user without one (or a brand-new record) still gets one.
  • The uniqueness walk ignores the user's own row, so the suffix is only reached on a genuine collision with somebody else.

An explicitly typed username still wins; a new user still gets one generated from their email; two different people whose emails share a local part still get maria and maria_1.

Also removes seven Logger.info calls left behind in user_form.ex from chasing this — they dumped the username at every step of validate / save / reload.

Verification

Checked against a running host application on real data, building the same changesets the admin form builds (nothing saved):

changesetbeforeafter
edit, no username paramusername change to fotkin_1no username change, field stays fotkin
edit, explicit usernamehonouredhonoured
brand-new usergenerated from emailgenerated from email

Regression tests added to test/phoenix_kit/users/user_org_changeset_test.exs (saved user keeps its name; explicit name wins; new user still generated; a real collision still gets a suffix). They are :integration — the environment this was written in has no PostgreSQL for the core suite, so they were not executed here; please run them with the suite.

…hangeset
Opening a user in the admin form and saving — toggling the password field is
enough — renamed them: maria became maria_1, then maria_2.
Two causes, both here. maybe_generate_username_from_email/1 runs on every
registration changeset and generates a username whenever the params carry none,
which is exactly what the admin form sends when it rebuilds the changeset for an
already-saved user. And ensure_unique_username/2 asked the database whether the
generated name was taken without excluding the user it was generating for, so
maria's own row made maria look unavailable to maria.
Generation is now skipped for a saved user that already has a username, and the
uniqueness walk ignores the user's own row. An explicitly typed username still
wins, a new user still gets one generated from their email, and a genuine
collision with someone else's name still gets its suffix.
Also removes seven Logger.info calls left in the admin user form from chasing
this: they dumped the username at every step of validate/save/reload.
Verified against the running application on real data: rebuilding the edit
changeset with no username param now produces no username change (it produced
fotkin_1 before). Regression tests added to user_org_changeset_test.exs — they
are :integration and were not run here, this environment has no PostgreSQL for
the core suite.
@ddon
ddon merged commit 118c34f into BeamLabEU:mainJul 29, 2026
ddon pushed a commit that referenced this pull request Jul 29, 2026
Impersonation (#672) documented "every attempt written to the activity feed
before the session changes" and delivered neither half: the row was written
only on success, only after the token had been minted, and alongside a second
`session.account_added` row indistinguishable from a user adding an account of
their own. Refusals — an Admin reaching for the Owner's account, an Admin
reaching sideways, a non-staff user hitting the endpoint — left no trace at
all, which is the entry a security review actually goes looking for. Refusals
now write `session.impersonation_refused` with the deciding rule, carrying no
target_uuid so they stay a feed entry rather than a message to the account they
named; successes write one row, and it says impersonated.
The controller resolved the uuid before checking authority, so its deliberately
precise operator copy told every signed-in user which uuids were live accounts.
`may_impersonate?/1` settles authority first and shares `staff?/1` with
`authorize_impersonation/2` so the rules cannot drift.
Both session actions reach the target's inbox via target_uuid but were claimed
by no notification type and had no Render clause, so the customer's bell read
"Session impersonated" and could not be switched off. Claimed by `security`,
given recipient-facing copy.
`already_intercepted` (#670) was independent of `skip_queue`, so a worker that
set one and forgot the other handed its own dequeued job back to the queue. It
can only mean "a worker is re-sending", so it now implies skip_queue.
A generated username (#671) is put after `validate_username/2` has already run,
so it reached the database with no uniqueness guard; two registrations racing on
the same local part raised `Ecto.ConstraintError` out of `Repo.insert/1`. The
constraint is re-attached to the generation branch.
Reviews in dev_docs/pull_requests/2026/{670,671,672}-*/CLAUDE_REVIEW.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timujinne
timujinne deleted the fix/username-rewritten-on-edit branch August 6, 2026 05:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@timujinne@ddon