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
|
|
@ -48,6 +48,24 @@ type usernameAvailabilityStore interface {
|
|||
CheckUsername(ctx context.Context, userID int64, username string) (bool, error)
|
||||
}
|
||||
|
||||
// adminUsernameAvailabilityStore is usernameAvailabilityStore's admin-bypass
|
||||
// counterpart: CheckUsernameAdmin skips the operator reserved-username
|
||||
// blocklist, so UpdateUsernameAdmin can hand a deliberately reserved word to
|
||||
// a specific account instead of that same reservation blocking the operator's
|
||||
// own assignment.
|
||||
type adminUsernameAvailabilityStore interface {
|
||||
CheckUsernameAdmin(ctx context.Context, userID int64, username string) (bool, error)
|
||||
}
|
||||
|
||||
// adminUsernameStore is store.UserStore's admin-bypass counterpart for the
|
||||
// actual write: UpdateUsername's own write path enforces the reserved-word
|
||||
// blocklist a second time (independent of the availability check), so
|
||||
// bypassing only checkUsernameAvailable isn't enough -- the write itself
|
||||
// needs UpdateUsernameAdmin too.
|
||||
type adminUsernameStore interface {
|
||||
UpdateUsernameAdmin(ctx context.Context, userID int64, username string) (domain.User, error)
|
||||
}
|
||||
|
||||
type moderationFlagAudienceStore interface {
|
||||
ModerationFlagAudience(ctx context.Context, userID int64, limit int) ([]int64, error)
|
||||
}
|
||||
|
|
@ -318,6 +336,17 @@ func (s *Service) checkUsernameAvailable(ctx context.Context, selfID int64, user
|
|||
return !found || u.ID == selfID, nil
|
||||
}
|
||||
|
||||
// checkUsernameAvailableAdmin is checkUsernameAvailable without the operator
|
||||
// reserved-username blocklist. Falls back to checkUsernameAvailable (which
|
||||
// does enforce it) if the store doesn't implement the admin-bypass method --
|
||||
// availability checking still works, just without the bypass.
|
||||
func (s *Service) checkUsernameAvailableAdmin(ctx context.Context, selfID int64, username string) (bool, error) {
|
||||
if checker, ok := s.users.(adminUsernameAvailabilityStore); ok {
|
||||
return checker.CheckUsernameAdmin(ctx, selfID, username)
|
||||
}
|
||||
return s.checkUsernameAvailable(ctx, selfID, username)
|
||||
}
|
||||
|
||||
// UpdateUsername 修改当前用户的主 username(self-service)。空字符串表示删除 username。
|
||||
func (s *Service) UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error) {
|
||||
return s.updateUsername(ctx, userID, username, true)
|
||||
|
|
@ -350,7 +379,15 @@ func (s *Service) updateUsername(ctx context.Context, userID int64, username str
|
|||
if !validUsername(username) || (enforceReserved && s.reserved.Contains(username)) {
|
||||
return domain.User{}, domain.ErrUsernameInvalid
|
||||
}
|
||||
ok, err := s.checkUsernameAvailable(ctx, self.ID, username)
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
)
|
||||
if enforceReserved {
|
||||
ok, err = s.checkUsernameAvailable(ctx, self.ID, username)
|
||||
} else {
|
||||
ok, err = s.checkUsernameAvailableAdmin(ctx, self.ID, username)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
|
|
@ -358,7 +395,14 @@ func (s *Service) updateUsername(ctx context.Context, userID int64, username str
|
|||
return domain.User{}, domain.ErrUsernameOccupied
|
||||
}
|
||||
}
|
||||
u, err := s.users.UpdateUsername(ctx, self.ID, username)
|
||||
var u domain.User
|
||||
if enforceReserved {
|
||||
u, err = s.users.UpdateUsername(ctx, self.ID, username)
|
||||
} else if admin, ok := s.users.(adminUsernameStore); ok {
|
||||
u, err = admin.UpdateUsernameAdmin(ctx, self.ID, username)
|
||||
} else {
|
||||
u, err = s.users.UpdateUsername(ctx, self.ID, username)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue