+// HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
+// An update can be a mix of these actions:
+// * Create user
+// * Delete user
+// * Modify user (including renaming the account login)
+//
+// The Transaction sent by the client includes one data field per user that was modified. This data field in turn
+// contains another data field encoded in its payload with a varying number of sub fields depending on which action is
+// performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
+func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+ for _, field := range t.Fields {
+ subFields, err := ReadFields(field.Data[0:2], field.Data[2:])
+ if err != nil {
+ return res, err
+ }
+
+ // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
+ if len(subFields) == 1 {
+ if !cc.Authorize(accessDeleteUser) {
+ res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
+ return res, err
+ }
+
+ login := string(encodeString(getField(FieldData, &subFields).Data))
+ cc.logger.Info("DeleteUser", "login", login)
+
+ if err := cc.Server.DeleteUser(login); err != nil {
+ return res, err
+ }
+ continue
+ }
+
+ // login of the account to update
+ var accountToUpdate, loginToRename string
+
+ // If FieldData is included, this is a rename operation where FieldData contains the login of the existing
+ // account and FieldUserLogin contains the new login.
+ if getField(FieldData, &subFields) != nil {
+ loginToRename = string(encodeString(getField(FieldData, &subFields).Data))
+ }
+ userLogin := string(encodeString(getField(FieldUserLogin, &subFields).Data))
+ if loginToRename != "" {
+ accountToUpdate = loginToRename
+ } else {
+ accountToUpdate = userLogin
+ }
+
+ // Check if accountToUpdate has an existing account. If so, we know we are updating an existing user.
+ if acc, ok := cc.Server.Accounts[accountToUpdate]; ok {
+ if loginToRename != "" {
+ cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
+ } else {
+ cc.logger.Info("UpdateUser", "login", accountToUpdate)
+ }
+
+ // account exists, so this is an update action
+ if !cc.Authorize(accessModifyUser) {
+ res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
+ return res, nil
+ }
+
+ // This part is a bit tricky. There are three possibilities:
+ // 1) The transaction is intended to update the password.
+ // In this case, FieldUserPassword is sent with the new password.
+ // 2) The transaction is intended to remove the password.
+ // In this case, FieldUserPassword is not sent.
+ // 3) The transaction updates the users access bits, but not the password.
+ // In this case, FieldUserPassword is sent with zero as the only byte.
+ if getField(FieldUserPassword, &subFields) != nil {
+ newPass := getField(FieldUserPassword, &subFields).Data
+ if !bytes.Equal([]byte{0}, newPass) {
+ acc.Password = hashAndSalt(newPass)
+ }
+ } else {
+ acc.Password = hashAndSalt([]byte(""))
+ }
+
+ if getField(FieldUserAccess, &subFields) != nil {
+ copy(acc.Access[:], getField(FieldUserAccess, &subFields).Data)
+ }
+
+ err = cc.Server.UpdateUser(
+ string(encodeString(getField(FieldData, &subFields).Data)),
+ string(encodeString(getField(FieldUserLogin, &subFields).Data)),
+ string(getField(FieldUserName, &subFields).Data),
+ acc.Password,
+ acc.Access,
+ )
+ if err != nil {
+ return res, err
+ }
+ } else {
+ if !cc.Authorize(accessCreateUser) {
+ res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
+ return res, nil
+ }
+
+ cc.logger.Info("CreateUser", "login", userLogin)
+
+ newAccess := accessBitmap{}
+ copy(newAccess[:], getField(FieldUserAccess, &subFields).Data)
+
+ // Prevent account from creating new account with greater permission
+ for i := 0; i < 64; i++ {
+ if newAccess.IsSet(i) {
+ if !cc.Authorize(i) {
+ return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), nil
+ }
+ }
+ }
+
+ err = cc.Server.NewUser(userLogin, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
+ if err != nil {
+ return append(res, cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")), nil
+ }
+ }
+ }
+
+ res = append(res, cc.NewReply(t))
+ return res, err
+}
+