From 4e9ddf78ab8162c3a5a01b9278060550b8c59a86 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 12 Oct 2021 09:11:14 +0200 Subject: [PATCH] service: allow updating other users --- doc/soju.1.scd | 16 ++++++++--- service.go | 73 +++++++++++++++++++++++++++++++++++++++++--------- user.go | 26 ++++++++++++++++++ 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/doc/soju.1.scd b/doc/soju.1.scd index d244477..03b6146 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -322,16 +322,24 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just Other options are: - *-admin* + *-admin* true|false Make the new user an administrator. *-realname* Set the user's realname. This is used as a fallback if there is no realname set for a network. -*user update* [-password ] [-realname ] - Update the current user. The options are the same as the _user create_ - command. +*user update* [username] [options...] + Update a user. The options are the same as the _user create_ command. + + If _username_ is omitted, the current user is updated. Only admins can + update other users. + + Not all flags are valid in all contexts: + + - The _-username_ flag is never valid, usernames are immutable. + - The _-realname_ flag is only valid when updating the current user. + - The _-admin_ flag is only valid when updating another user. *user delete* Delete a soju user. Only admins can delete accounts. diff --git a/service.go b/service.go index cca1f5a..b7da332 100644 --- a/service.go +++ b/service.go @@ -125,7 +125,7 @@ func handleServicePRIVMSG(dc *downstreamConn, text string) { return } if cmd.admin && !dc.user.Admin { - sendServicePRIVMSG(dc, fmt.Sprintf(`error: you must be an admin to use this command`)) + sendServicePRIVMSG(dc, "error: you must be an admin to use this command") return } @@ -766,35 +766,84 @@ func handleUserCreate(dc *downstreamConn, params []string) error { return nil } +func popArg(params []string) (string, []string) { + if len(params) > 0 && !strings.HasPrefix(params[0], "-") { + return params[0], params[1:] + } + return "", params +} + func handleUserUpdate(dc *downstreamConn, params []string) error { var password, realname *string + var admin *bool fs := newFlagSet() fs.Var(stringPtrFlag{&password}, "password", "") fs.Var(stringPtrFlag{&realname}, "realname", "") + fs.Var(boolPtrFlag{&admin}, "admin", "") + username, params := popArg(params) if err := fs.Parse(params); err != nil { return err } + if len(fs.Args()) > 0 { + return fmt.Errorf("unexpected argument") + } - // copy the user record because we'll mutate it - record := dc.user.User - + var hashed *string if password != nil { - hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost) + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("failed to hash password: %v", err) } - record.Password = string(hashed) - } - if realname != nil { - record.Realname = *realname + hashedStr := string(hashedBytes) + hashed = &hashedStr } - if err := dc.user.updateUser(&record); err != nil { - return err + if username != "" && username != dc.user.Username { + if !dc.user.Admin { + return fmt.Errorf("you must be an admin to update other users") + } + if realname != nil { + return fmt.Errorf("cannot update -realname of other user") + } + + u := dc.srv.getUser(username) + if u == nil { + return fmt.Errorf("unknown username %q", username) + } + + done := make(chan error, 1) + u.events <- eventUserUpdate{ + password: hashed, + admin: admin, + done: done, + } + if err := <-done; err != nil { + return err + } + + sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", username)) + } else { + // copy the user record because we'll mutate it + record := dc.user.User + + if hashed != nil { + record.Password = *hashed + } + if realname != nil { + record.Realname = *realname + } + if admin != nil { + return fmt.Errorf("cannot update -admin of own user") + } + + if err := dc.user.updateUser(&record); err != nil { + return err + } + + sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username)) } - sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username)) return nil } diff --git a/user.go b/user.go index 966ba80..b358a66 100644 --- a/user.go +++ b/user.go @@ -59,6 +59,12 @@ type eventBroadcast struct { type eventStop struct{} +type eventUserUpdate struct { + password *string + admin *bool + done chan error +} + type deliveredClientMap map[string]string // client name -> msg ID type deliveredStore struct { @@ -642,6 +648,26 @@ func (u *user) run() { u.forEachDownstream(func(dc *downstreamConn) { dc.SendMessage(msg) }) + case eventUserUpdate: + // copy the user record because we'll mutate it + record := u.User + + if e.password != nil { + record.Password = *e.password + } + if e.admin != nil { + record.Admin = *e.admin + } + + e.done <- u.updateUser(&record) + + // If the password was updated, kill all downstream connections to + // force them to re-authenticate with the new credentials. + if e.password != nil { + u.forEachDownstream(func(dc *downstreamConn) { + dc.Close() + }) + } case eventStop: u.forEachDownstream(func(dc *downstreamConn) { dc.Close()