]> git.r.bdr.sh - rbdr/mobius/commitdiff
Implement multi-account edit
authorJeff Halter <redacted>
Sun, 5 Jun 2022 03:14:16 +0000 (20:14 -0700)
committerJeff Halter <redacted>
Sun, 5 Jun 2022 03:14:16 +0000 (20:14 -0700)
hotline/account.go
hotline/field.go
hotline/server.go
hotline/transaction.go
hotline/transaction_handlers.go
hotline/transaction_handlers_test.go
hotline/version.go

index 736859210980c2c13851d0e1834567ec38ceeb31..3da818f1ddf5a59e1312ed5ea1c7697e53c6dd27 100644 (file)
@@ -1,7 +1,9 @@
 package hotline
 
 import (
+       "encoding/binary"
        "github.com/jhalter/mobius/concat"
+       "golang.org/x/crypto/bcrypt"
 )
 
 const GuestAccount = "guest" // default account used when no login is provided for a connection
@@ -15,10 +17,26 @@ type Account struct {
 
 // MarshalBinary marshals an Account to byte slice
 func (a *Account) MarshalBinary() (out []byte) {
+       fields := []Field{
+               NewField(fieldUserName, []byte(a.Name)),
+               NewField(fieldUserLogin, negateString([]byte(a.Login))),
+               NewField(fieldUserAccess, *a.Access),
+       }
+
+       if bcrypt.CompareHashAndPassword([]byte(a.Password), []byte("")) != nil {
+               fields = append(fields, NewField(fieldUserPassword, []byte("x")))
+       }
+
+       fieldCount := make([]byte, 2)
+       binary.BigEndian.PutUint16(fieldCount, uint16(len(fields)))
+
+       var fieldPayload []byte
+       for _, field := range fields {
+               fieldPayload = append(fieldPayload, field.Payload()...)
+       }
+
        return concat.Slices(
-               []byte{0x00, 0x3}, // param count -- always 3
-               NewField(fieldUserName, []byte(a.Name)).Payload(),
-               NewField(fieldUserLogin, negateString([]byte(a.Login))).Payload(),
-               NewField(fieldUserAccess, *a.Access).Payload(),
+               fieldCount,
+               fieldPayload,
        )
 }
index 5f2e87354b4c4e0c188c9e0da70693baab15f0a8..2be4c4a7b96faff1f74f3f47782096eb69c8fa06 100644 (file)
@@ -90,3 +90,12 @@ func NewField(id uint16, data []byte) Field {
 func (f Field) Payload() []byte {
        return concat.Slices(f.ID, f.FieldSize, f.Data)
 }
+
+func getField(id int, fields *[]Field) *Field {
+       for _, field := range *fields {
+               if id == int(binary.BigEndian.Uint16(field.ID)) {
+                       return &field
+               }
+       }
+       return nil
+}
index 0828385dc2ed9027f13075605af634ee0a399490..aebff6ed8af71e73b4534b631fb127ea0d27eb00 100644 (file)
@@ -359,6 +359,38 @@ func (s *Server) NewUser(login, name, password string, access []byte) error {
        return FS.WriteFile(s.ConfigDir+"Users/"+login+".yaml", out, 0666)
 }
 
+func (s *Server) UpdateUser(login, newLogin, name, password string, access []byte) error {
+       s.mux.Lock()
+       defer s.mux.Unlock()
+
+       fmt.Printf("login: %v, newLogin: %v: ", login, newLogin)
+
+       // update renames the user login
+       if login != newLogin {
+               err := os.Rename(s.ConfigDir+"Users/"+login+".yaml", s.ConfigDir+"Users/"+newLogin+".yaml")
+               if err != nil {
+                       return err
+               }
+               s.Accounts[newLogin] = s.Accounts[login]
+               delete(s.Accounts, login)
+       }
+
+       account := s.Accounts[newLogin]
+       account.Access = &access
+       account.Name = name
+       account.Password = password
+
+       out, err := yaml.Marshal(&account)
+       if err != nil {
+               return err
+       }
+       if err := os.WriteFile(s.ConfigDir+"Users/"+newLogin+".yaml", out, 0666); err != nil {
+               return err
+       }
+
+       return nil
+}
+
 // DeleteUser deletes the user account
 func (s *Server) DeleteUser(login string) error {
        s.mux.Lock()
index 2994785cca1194c6352ad29281e8090907dfde60..04fb4b6520cb51a884ca4f7b86e9c208870d6716 100644 (file)
@@ -44,14 +44,14 @@ const (
        tranDownloadFldr         = 210
        // tranDownloadInfo         = 211 TODO: implement file transfer queue
        // tranDownloadBanner     = 212 TODO: figure out what this is used for
-       tranUploadFldr        = 213
-       tranGetUserNameList   = 300
-       tranNotifyChangeUser  = 301
-       tranNotifyDeleteUser  = 302
-       tranGetClientInfoText = 303
-       tranSetClientUserInfo = 304
-       tranListUsers         = 348
-       // tranUpdateUser         = 349 TODO: implement user updates from the > 1.5 account editor
+       tranUploadFldr         = 213
+       tranGetUserNameList    = 300
+       tranNotifyChangeUser   = 301
+       tranNotifyDeleteUser   = 302
+       tranGetClientInfoText  = 303
+       tranSetClientUserInfo  = 304
+       tranListUsers          = 348
+       tranUpdateUser         = 349
        tranNewUser            = 350
        tranDeleteUser         = 351
        tranGetUser            = 352
index ec7910a49fdad6af34d9d5c551ef367934eb4d3e..90ca6de50ec409d8cd905cbbc87aff62dd48a930 100644 (file)
@@ -210,6 +210,11 @@ var TransactionHandlers = map[uint16]TransactionType{
                Name:    "tranNewUser",
                Handler: HandleNewUser,
        },
+       tranUpdateUser: {
+               Access:  accessAlwaysAllow,
+               Name:    "tranUpdateUser",
+               Handler: HandleUpdateUser,
+       },
        tranOldPostNews: {
                Access:  accessNewsPostArt,
                DenyMsg: "You are not allowed to post news.",
@@ -615,12 +620,11 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                account.Password = hashAndSalt(t.GetField(fieldUserPassword).Data)
        }
 
-       file := cc.Server.ConfigDir + "Users/" + login + ".yaml"
        out, err := yaml.Marshal(&account)
        if err != nil {
                return res, err
        }
-       if err := ioutil.WriteFile(file, out, 0666); err != nil {
+       if err := os.WriteFile(cc.Server.ConfigDir+"Users/"+login+".yaml", out, 0666); err != nil {
                return res, err
        }
 
@@ -683,7 +687,6 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err
        }
 
        var userFields []Field
-       // TODO: make order deterministic
        for _, acc := range cc.Server.Accounts {
                userField := acc.MarshalBinary()
                userFields = append(userFields, NewField(fieldData, userField))
@@ -693,6 +696,94 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err
        return res, err
 }
 
+// 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 len(subFields) == 1 {
+                       login := DecodeUserString(getField(fieldData, &subFields).Data)
+                       cc.Server.Logger.Infow("DeleteUser", "login", login)
+
+                       if !authorize(cc.Account.Access, accessDeleteUser) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
+                               return res, err
+                       }
+
+                       if err := cc.Server.DeleteUser(login); err != nil {
+                               return res, err
+                       }
+                       continue
+               }
+
+               login := DecodeUserString(getField(fieldUserLogin, &subFields).Data)
+
+               // check if the login exists; if so, we know we are updating an existing user
+               if acc, ok := cc.Server.Accounts[login]; ok {
+                       cc.Server.Logger.Infow("UpdateUser", "login", login)
+
+                       // account exists, so this is an update action
+                       if !authorize(cc.Account.Access, accessModifyUser) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
+                               return res, err
+                       }
+
+                       if getField(fieldUserPassword, &subFields) != nil {
+                               newPass := getField(fieldUserPassword, &subFields).Data
+                               acc.Password = hashAndSalt(newPass)
+                       } else {
+                               acc.Password = hashAndSalt([]byte(""))
+                       }
+
+                       if getField(fieldUserAccess, &subFields) != nil {
+                               acc.Access = &getField(fieldUserAccess, &subFields).Data
+                       }
+
+                       err = cc.Server.UpdateUser(
+                               DecodeUserString(getField(fieldData, &subFields).Data),
+                               DecodeUserString(getField(fieldUserLogin, &subFields).Data),
+                               string(getField(fieldUserName, &subFields).Data),
+                               acc.Password,
+                               *acc.Access,
+                       )
+                       if err != nil {
+                               return res, err
+                       }
+               } else {
+                       cc.Server.Logger.Infow("CreateUser", "login", login)
+
+                       if !authorize(cc.Account.Access, accessCreateUser) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
+                               return res, err
+                       }
+
+                       err := cc.Server.NewUser(
+                               login,
+                               string(getField(fieldUserName, &subFields).Data),
+                               string(getField(fieldUserPassword, &subFields).Data),
+                               getField(fieldUserAccess, &subFields).Data,
+                       )
+                       if err != nil {
+                               return []Transaction{}, err
+                       }
+               }
+       }
+
+       res = append(res, cc.NewReply(t))
+       return res, err
+}
+
 // HandleNewUser creates a new user account
 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !authorize(cc.Account.Access, accessCreateUser) {
index 48e0f0e2e0930b197e1228319bd70fa093f03a90..9165a6629196c6c3ae062c74fb01f27a7089f3ab 100644 (file)
@@ -1731,3 +1731,179 @@ func TestHandleDownloadFile(t *testing.T) {
                })
        }
 }
+
+func TestHandleUpdateUser(t *testing.T) {
+       type args struct {
+               cc *ClientConn
+               t  *Transaction
+       }
+       tests := []struct {
+               name    string
+               args    args
+               wantRes []Transaction
+               wantErr assert.ErrorAssertionFunc
+       }{
+               {
+                       name: "when action is create user without required permission",
+                       args: args{
+                               cc: &ClientConn{
+                                       Server: &Server{
+                                               Logger: NewTestLogger(),
+                                       },
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                               },
+                               t: NewTransaction(
+                                       tranUpdateUser,
+                                       &[]byte{0, 0},
+                                       NewField(fieldData, []byte{
+                                               0x00, 0x04, // field count
+
+                                               0x00, 0x69, // fieldUserLogin = 105
+                                               0x00, 0x03,
+                                               0x9d, 0x9d, 0x9d,
+
+                                               0x00, 0x6a, // fieldUserPassword = 106
+                                               0x00, 0x03,
+                                               0x9c, 0x9c, 0x9c,
+
+                                               0x00, 0x66, // fieldUserName = 102
+                                               0x00, 0x03,
+                                               0x61, 0x61, 0x61,
+
+                                               0x00, 0x6e, // fieldUserAccess = 110
+                                               0x00, 0x08,
+                                               0x60, 0x70, 0x0c, 0x20, 0x03, 0x80, 0x00, 0x00,
+                                       }),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0, 0x00},
+                                       ID:        []byte{0x9a, 0xcb, 0x04, 0x42},
+                                       ErrorCode: []byte{0, 0, 0, 1},
+                                       Fields: []Field{
+                                               NewField(fieldError, []byte("You are not allowed to create new accounts.")),
+                                       },
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
+               {
+                       name: "when action is modify user without required permission",
+                       args: args{
+                               cc: &ClientConn{
+                                       Server: &Server{
+                                               Logger: NewTestLogger(),
+                                               Accounts: map[string]*Account{
+                                                       "bbb": {},
+                                               },
+                                       },
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                               },
+                               t: NewTransaction(
+                                       tranUpdateUser,
+                                       &[]byte{0, 0},
+                                       NewField(fieldData, []byte{
+                                               0x00, 0x04, // field count
+
+                                               0x00, 0x69, // fieldUserLogin = 105
+                                               0x00, 0x03,
+                                               0x9d, 0x9d, 0x9d,
+
+                                               0x00, 0x6a, // fieldUserPassword = 106
+                                               0x00, 0x03,
+                                               0x9c, 0x9c, 0x9c,
+
+                                               0x00, 0x66, // fieldUserName = 102
+                                               0x00, 0x03,
+                                               0x61, 0x61, 0x61,
+
+                                               0x00, 0x6e, // fieldUserAccess = 110
+                                               0x00, 0x08,
+                                               0x60, 0x70, 0x0c, 0x20, 0x03, 0x80, 0x00, 0x00,
+                                       }),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0, 0x00},
+                                       ID:        []byte{0x9a, 0xcb, 0x04, 0x42},
+                                       ErrorCode: []byte{0, 0, 0, 1},
+                                       Fields: []Field{
+                                               NewField(fieldError, []byte("You are not allowed to modify accounts.")),
+                                       },
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
+               {
+                       name: "when action is delete user without required permission",
+                       args: args{
+                               cc: &ClientConn{
+                                       Server: &Server{
+                                               Logger: NewTestLogger(),
+                                               Accounts: map[string]*Account{
+                                                       "bbb": {},
+                                               },
+                                       },
+                                       Account: &Account{
+                                               Access: func() *[]byte {
+                                                       var bits accessBitmap
+                                                       access := bits[:]
+                                                       return &access
+                                               }(),
+                                       },
+                               },
+                               t: NewTransaction(
+                                       tranUpdateUser,
+                                       &[]byte{0, 0},
+                                       NewField(fieldData, []byte{
+                                               0x00, 0x01,
+                                               0x00, 0x65,
+                                               0x00, 0x03,
+                                               0x88, 0x9e, 0x8b,
+                                       }),
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       Flags:     0x00,
+                                       IsReply:   0x01,
+                                       Type:      []byte{0, 0x00},
+                                       ID:        []byte{0x9a, 0xcb, 0x04, 0x42},
+                                       ErrorCode: []byte{0, 0, 0, 1},
+                                       Fields: []Field{
+                                               NewField(fieldError, []byte("You are not allowed to delete accounts.")),
+                                       },
+                               },
+                       },
+                       wantErr: assert.NoError,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       gotRes, err := HandleUpdateUser(tt.args.cc, tt.args.t)
+                       if !tt.wantErr(t, err, fmt.Sprintf("HandleUpdateUser(%v, %v)", tt.args.cc, tt.args.t)) {
+                               return
+                       }
+
+                       tranAssertEqual(t, tt.wantRes, gotRes)
+               })
+       }
+}
index 4ca8b96792e69e42da67facdfd558421d1373840..a2b5cfe11962f266e18f6fc66e15e6b3f5db06a4 100644 (file)
@@ -1,3 +1,3 @@
 package hotline
 
-const VERSION = "0.3.0"
+const VERSION = "0.4.0"