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

@ -163,10 +163,25 @@ func (s *UserStore) CheckUsername(_ context.Context, userID int64, username stri
if s.usernameRegistry != nil && s.usernameRegistry.nameReserved(username) {
return false, nil
}
return s.usernameAvailableIgnoringReserved(userID, username)
}
// CheckUsernameAdmin is CheckUsername without the operator reserved-username
// blocklist check, for the admin console deliberately assigning a reserved
// word to a specific account.
func (s *UserStore) CheckUsernameAdmin(_ context.Context, userID int64, username string) (bool, error) {
username = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
if username == "" {
return true, nil
}
return s.usernameAvailableIgnoringReserved(userID, username)
}
func (s *UserStore) usernameAvailableIgnoringReserved(userID int64, usernameLower string) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for id, u := range s.byID {
if !u.Deleted && strings.ToLower(u.Username) == username && id != userID {
if !u.Deleted && strings.ToLower(u.Username) == usernameLower && id != userID {
return false, nil
}
}
@ -211,6 +226,17 @@ func (s *UserStore) Search(_ context.Context, currentUserID int64, query, phoneQ
}
func (s *UserStore) UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error) {
return s.updateUsernameChecked(ctx, userID, username, true)
}
// UpdateUsernameAdmin is UpdateUsername without the operator reserved-username
// blocklist check, for the admin console deliberately assigning a reserved
// word to a specific account.
func (s *UserStore) UpdateUsernameAdmin(ctx context.Context, userID int64, username string) (domain.User, error) {
return s.updateUsernameChecked(ctx, userID, username, false)
}
func (s *UserStore) updateUsernameChecked(ctx context.Context, userID int64, username string, checkReserved bool) (domain.User, error) {
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
usernameLower := strings.ToLower(username)
s.mu.Lock()
@ -227,7 +253,13 @@ func (s *UserStore) UpdateUsername(ctx context.Context, userID int64, username s
}
}
if s.usernameRegistry != nil {
if _, err := s.usernameRegistry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: userID}, username); err != nil {
var err error
if checkReserved {
_, err = s.usernameRegistry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: userID}, username)
} else {
_, err = s.usernameRegistry.SetEditableUsernameAdmin(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: userID}, username)
}
if err != nil {
return domain.User{}, err
}
}