admin: bypass the reserved-username blocklist at write time too

UpdateUsernameAdmin already skipped the reserved-word check in the
availability lookup, but UserStore.UpdateUsername's own write path
(replacePeerUsernameTx / CollectibleUsernameStore.SetEditableUsername)
enforces the same operator blocklist a second time, independently and
unconditionally. That second check is what was still rejecting an admin
handing out a word they'd deliberately reserved, with "username occupied".

Add UpdateUsernameAdmin/SetEditableUsernameAdmin bypass variants down the
write path (postgres and memory) and route users.Service's actual write
through them when the availability check was already bypassed.
This commit is contained in:
Astra 2026-09-15 16:22:00 +01:00
parent 53872f8fc9
commit ea17d7da0d
6 changed files with 180 additions and 15 deletions

View file

@ -151,6 +151,39 @@ func TestServiceUpdateUsernameAdminBypassesReserved(t *testing.T) {
}
}
// TestServiceUpdateUsernameAdminBypassesOperatorReservedTable locks in that
// UpdateUsernameAdmin also bypasses the *database-backed* reserved-usernames
// blocklist (the admin console's own Reserved Usernames feature), not just
// config.ReservedUsernames -- an operator who deliberately reserves a word
// via that feature must still be able to hand it to a specific account,
// instead of their own reservation blocking them with "username occupied".
func TestServiceUpdateUsernameAdminBypassesOperatorReservedTable(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
reserved := memory.NewReservedUsernameStore()
registry := memory.NewCollectibleUsernameStore().WithReservedUsernames(reserved)
userStore.AttachUsernameRegistry(registry)
if _, err := reserved.ReserveUsername(ctx, "durov", "brand protection", "operator"); err != nil {
t.Fatalf("reserve username: %v", err)
}
target, err := userStore.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000006", FirstName: "Target"})
if err != nil {
t.Fatalf("create target: %v", err)
}
svc := NewService(userStore)
if _, err := svc.UpdateUsername(ctx, target.ID, "durov"); !errors.Is(err, domain.ErrUsernameOccupied) {
t.Fatalf("self-service claim of operator-reserved username err = %v, want username occupied", err)
}
u, err := svc.UpdateUsernameAdmin(ctx, target.ID, "durov")
if err != nil {
t.Fatalf("admin claim of operator-reserved username: %v", err)
}
if u.Username != "durov" {
t.Fatalf("admin claim of operator-reserved username: got username %q, want %q", u.Username, "durov")
}
}
// marksbotOverrideStore wraps memory.UserStore to serve domain.VerifierBotUser()
// for a fixed username lookup, since memory.UserStore.Create always assigns an
// id from its own auto-increment sequence and can never produce the fixed