admin: allow assigning a reserved username to a user via the admin console
Add users.Service.UpdateUsernameAdmin, which skips the config.ReservedUsernames block that self-service UpdateUsername enforces. Operators need to be able to hand a reserved word to a specific account even though regular users can't claim it themselves.
This commit is contained in:
parent
2ffdb1beb5
commit
d83034e8dc
4 changed files with 50 additions and 4 deletions
|
|
@ -206,7 +206,12 @@ type UsersService interface {
|
||||||
SetVerified(ctx context.Context, userID int64, verified bool) (domain.User, error)
|
SetVerified(ctx context.Context, userID int64, verified bool) (domain.User, error)
|
||||||
SetScamFake(ctx context.Context, userID int64, scam, fake bool) (domain.User, error)
|
SetScamFake(ctx context.Context, userID int64, scam, fake bool) (domain.User, error)
|
||||||
SetSupport(ctx context.Context, userID int64, support bool) (domain.User, error)
|
SetSupport(ctx context.Context, userID int64, support bool) (domain.User, error)
|
||||||
UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error)
|
// UpdateUsernameAdmin sets a username on the operator's behalf, bypassing
|
||||||
|
// the config.ReservedUsernames block (see users.Service.UpdateUsernameAdmin) --
|
||||||
|
// self-service UpdateUsername enforces that list, but an operator
|
||||||
|
// deliberately assigning a reserved word to an account needs to be able
|
||||||
|
// to do so.
|
||||||
|
UpdateUsernameAdmin(ctx context.Context, userID int64, username string) (domain.User, error)
|
||||||
UpdateColor(ctx context.Context, userID int64, forProfile bool, color domain.PeerColor) (domain.User, error)
|
UpdateColor(ctx context.Context, userID int64, forProfile bool, color domain.PeerColor) (domain.User, error)
|
||||||
UpdateEmojiStatus(ctx context.Context, userID int64, status domain.UserEmojiStatus) (domain.User, error)
|
UpdateEmojiStatus(ctx context.Context, userID int64, status domain.UserEmojiStatus) (domain.User, error)
|
||||||
UpdateProfile(ctx context.Context, userID int64, update domain.UserProfileUpdate) (domain.User, error)
|
UpdateProfile(ctx context.Context, userID int64, update domain.UserProfileUpdate) (domain.User, error)
|
||||||
|
|
@ -1438,7 +1443,7 @@ func (s *Service) SetUsername(ctx context.Context, req SetUsernameRequest) (Comm
|
||||||
if req.DryRun {
|
if req.DryRun {
|
||||||
return CommandResult{Message: "dry-run completed", Details: details}, nil
|
return CommandResult{Message: "dry-run completed", Details: details}, nil
|
||||||
}
|
}
|
||||||
updated, err := s.users.UpdateUsername(ctx, req.UserID, username)
|
updated, err := s.users.UpdateUsernameAdmin(ctx, req.UserID, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CommandResult{}, err
|
return CommandResult{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -781,6 +781,10 @@ func (f *fakeUsersService) UpdateUsername(_ context.Context, userID int64, usern
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeUsersService) UpdateUsernameAdmin(ctx context.Context, userID int64, username string) (domain.User, error) {
|
||||||
|
return f.UpdateUsername(ctx, userID, username)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeUsersService) UpdateProfile(_ context.Context, userID int64, update domain.UserProfileUpdate) (domain.User, error) {
|
func (f *fakeUsersService) UpdateProfile(_ context.Context, userID int64, update domain.UserProfileUpdate) (domain.User, error) {
|
||||||
u, ok := f.users[userID]
|
u, ok := f.users[userID]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -318,8 +318,21 @@ func (s *Service) checkUsernameAvailable(ctx context.Context, selfID int64, user
|
||||||
return !found || u.ID == selfID, nil
|
return !found || u.ID == selfID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUsername 修改当前用户的主 username。空字符串表示删除 username。
|
// UpdateUsername 修改当前用户的主 username(self-service)。空字符串表示删除 username。
|
||||||
func (s *Service) UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error) {
|
func (s *Service) UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error) {
|
||||||
|
return s.updateUsername(ctx, userID, username, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUsernameAdmin sets a user's username on the operator's behalf (via
|
||||||
|
// the admin console). Unlike UpdateUsername, it does not consult
|
||||||
|
// config.ReservedUsernames -- an operator who deliberately reserved a word
|
||||||
|
// still needs to be able to hand it to a specific account. Validity and
|
||||||
|
// availability (no collision with another account) are still enforced.
|
||||||
|
func (s *Service) UpdateUsernameAdmin(ctx context.Context, userID int64, username string) (domain.User, error) {
|
||||||
|
return s.updateUsername(ctx, userID, username, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateUsername(ctx context.Context, userID int64, username string, enforceReserved bool) (domain.User, error) {
|
||||||
self, err := s.loadSelf(ctx, userID)
|
self, err := s.loadSelf(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.User{}, err
|
return domain.User{}, err
|
||||||
|
|
@ -334,7 +347,7 @@ func (s *Service) UpdateUsername(ctx context.Context, userID int64, username str
|
||||||
return s.projectOne(ctx, self.ID, self)
|
return s.projectOne(ctx, self.ID, self)
|
||||||
}
|
}
|
||||||
if username != "" {
|
if username != "" {
|
||||||
if !validUsername(username) || s.reserved.Contains(username) {
|
if !validUsername(username) || (enforceReserved && s.reserved.Contains(username)) {
|
||||||
return domain.User{}, domain.ErrUsernameInvalid
|
return domain.User{}, domain.ErrUsernameInvalid
|
||||||
}
|
}
|
||||||
ok, err := s.checkUsernameAvailable(ctx, self.ID, username)
|
ok, err := s.checkUsernameAvailable(ctx, self.ID, username)
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,30 @@ func TestServiceUsernameReservedBlocksNewClaimsButKeepsExisting(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServiceUpdateUsernameAdminBypassesReserved locks in that the admin
|
||||||
|
// console can deliberately assign a config.ReservedUsernames word to an
|
||||||
|
// account, even though self-service UpdateUsername refuses the same claim.
|
||||||
|
func TestServiceUpdateUsernameAdminBypassesReserved(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
store := memory.NewUserStore()
|
||||||
|
target, err := store.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000005", FirstName: "Target"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create target: %v", err)
|
||||||
|
}
|
||||||
|
svc := NewService(store, WithReservedUsernames([]string{"admin"}))
|
||||||
|
|
||||||
|
if _, err := svc.UpdateUsername(ctx, target.ID, "admin"); !errors.Is(err, domain.ErrUsernameInvalid) {
|
||||||
|
t.Fatalf("self-service claim of reserved username err = %v, want username invalid", err)
|
||||||
|
}
|
||||||
|
u, err := svc.UpdateUsernameAdmin(ctx, target.ID, "admin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin claim of reserved username: %v", err)
|
||||||
|
}
|
||||||
|
if u.Username != "admin" {
|
||||||
|
t.Fatalf("admin claim of reserved username: got username %q, want %q", u.Username, "admin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// marksbotOverrideStore wraps memory.UserStore to serve domain.VerifierBotUser()
|
// marksbotOverrideStore wraps memory.UserStore to serve domain.VerifierBotUser()
|
||||||
// for a fixed username lookup, since memory.UserStore.Create always assigns an
|
// 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
|
// id from its own auto-increment sequence and can never produce the fixed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue