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:
parent
53872f8fc9
commit
ea17d7da0d
6 changed files with 180 additions and 15 deletions
|
|
@ -81,10 +81,21 @@ func usernameReservedTx(ctx context.Context, db sqlcgen.DBTX, usernameLower stri
|
|||
}
|
||||
|
||||
func peerUsernameAvailable(ctx context.Context, db sqlcgen.DBTX, usernameLower, peerType string, peerID int64) (bool, error) {
|
||||
if reserved, err := usernameReservedTx(ctx, db, usernameLower); err != nil {
|
||||
return false, err
|
||||
} else if reserved {
|
||||
return false, nil
|
||||
return peerUsernameAvailableChecked(ctx, db, usernameLower, peerType, peerID, true)
|
||||
}
|
||||
|
||||
// peerUsernameAvailableChecked is peerUsernameAvailable with the operator
|
||||
// blocklist check optional: an operator deliberately reserving a word still
|
||||
// needs to be able to hand it to a specific account via the admin console,
|
||||
// so the admin-initiated username-set path skips it (checkReserved=false)
|
||||
// while self-service username changes always enforce it.
|
||||
func peerUsernameAvailableChecked(ctx context.Context, db sqlcgen.DBTX, usernameLower, peerType string, peerID int64, checkReserved bool) (bool, error) {
|
||||
if checkReserved {
|
||||
if reserved, err := usernameReservedTx(ctx, db, usernameLower); err != nil {
|
||||
return false, err
|
||||
} else if reserved {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
owner, found, err := getPeerUsernameOwner(ctx, db, usernameLower, false)
|
||||
if err != nil || !found {
|
||||
|
|
@ -134,11 +145,23 @@ WHERE peer_type = $1
|
|||
// collectible_usernames and must survive every client-driven username edit,
|
||||
// otherwise account.updateUsername would silently release a minted asset.
|
||||
func replacePeerUsernameTx(ctx context.Context, tx pgx.Tx, peerType string, peerID int64, username, usernameLower string) error {
|
||||
return replacePeerUsernameTxChecked(ctx, tx, peerType, peerID, username, usernameLower, true)
|
||||
}
|
||||
|
||||
// replacePeerUsernameTxChecked is replacePeerUsernameTx with the operator
|
||||
// blocklist check optional: the admin-initiated username-set path
|
||||
// (UserStore.UpdateUsernameAdmin) skips it so an operator can deliberately
|
||||
// hand a reserved word to a specific account, while every other caller
|
||||
// (self-service, bots, channel settings, account deletion) always enforces
|
||||
// it via replacePeerUsernameTx.
|
||||
func replacePeerUsernameTxChecked(ctx context.Context, tx pgx.Tx, peerType string, peerID int64, username, usernameLower string, checkReserved bool) error {
|
||||
if usernameLower != "" {
|
||||
if reserved, err := usernameReservedTx(ctx, tx, usernameLower); err != nil {
|
||||
return err
|
||||
} else if reserved {
|
||||
return domain.ErrUsernameOccupied
|
||||
if checkReserved {
|
||||
if reserved, err := usernameReservedTx(ctx, tx, usernameLower); err != nil {
|
||||
return err
|
||||
} else if reserved {
|
||||
return domain.ErrUsernameOccupied
|
||||
}
|
||||
}
|
||||
owner, found, err := getPeerUsernameOwner(ctx, tx, usernameLower, true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,17 @@ func (s *UserStore) CheckUsername(ctx context.Context, userID int64, username st
|
|||
return peerUsernameAvailable(ctx, s.db, usernameLower, peerUsernameTypeUser, userID)
|
||||
}
|
||||
|
||||
// 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(ctx context.Context, userID int64, username string) (bool, error) {
|
||||
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
|
||||
if usernameLower == "" {
|
||||
return true, nil
|
||||
}
|
||||
return peerUsernameAvailableChecked(ctx, s.db, usernameLower, peerUsernameTypeUser, userID, false)
|
||||
}
|
||||
|
||||
func (s *UserStore) Search(ctx context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, error) {
|
||||
query = strings.ToLower(strings.TrimSpace(query))
|
||||
if currentUserID == 0 || query == "" {
|
||||
|
|
@ -257,6 +268,17 @@ func (s *UserStore) UpdatePhone(ctx context.Context, userID int64, phone string)
|
|||
}
|
||||
|
||||
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)
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
|
|
@ -281,7 +303,7 @@ func (s *UserStore) UpdateUsername(ctx context.Context, userID int64, username s
|
|||
}
|
||||
return domain.User{}, fmt.Errorf("lock user for username update: %w", err)
|
||||
}
|
||||
if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeUser, userID, username, usernameLower); err != nil {
|
||||
if err := replacePeerUsernameTxChecked(ctx, tx, peerUsernameTypeUser, userID, username, usernameLower, checkReserved); err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
row, err := qtx.UpdateUserUsername(ctx, sqlcgen.UpdateUserUsernameParams{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue