♻️ Simplify user storage

This commit is contained in:
Andreas Schneider 2020-10-10 19:06:49 +02:00
parent 9cef5c63a2
commit e6a62bfeb1
2 changed files with 68 additions and 79 deletions

139
store.go
View File

@ -26,6 +26,7 @@
package main package main
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -65,7 +66,8 @@ const (
type User struct { type User struct {
Username string Username string
Password string Password string `json:"-"`
PasswordHash string `json:"Password"`
Role GlobalRole Role GlobalRole
} }
@ -96,18 +98,6 @@ func (u User) key() string {
return usersPrefix + u.Username return usersPrefix + u.Username
} }
func (u User) prefix() string {
return fmt.Sprintf("user:%s", u.Username)
}
func (u User) roleKey() string {
return u.prefix() + ":role"
}
func (u User) passwordKey() string {
return u.prefix() + ":password"
}
func NewDBStore(filename string) (*DBStore, error) { func NewDBStore(filename string) (*DBStore, error) {
db, err := buntdb.Open(filename) db, err := buntdb.Open(filename)
if err != nil { if err != nil {
@ -128,24 +118,21 @@ var ErrUserExists = errors.New("user already exists")
var ErrUserNotFound = errors.New("user not found") var ErrUserNotFound = errors.New("user not found")
var ErrInvalidUsername = errors.New("invalid username") var ErrInvalidUsername = errors.New("invalid username")
func (store *DBStore) setUserValues(tx *buntdb.Tx, user User) (exists bool, err error) { func (u User) merge(updates User) (User, error) {
pwString, err := buildPassword(user.Username, user.Password) merged := u
if updates.Password != "" {
pwHash, err := hashPassword(updates.Password)
if err != nil { if err != nil {
return false, fmt.Errorf("cannot hash password: %w", err) return u, fmt.Errorf("cannot hash password: %w", err)
}
merged.PasswordHash = pwHash
}
if updates.Role != "" {
merged.Role = updates.Role
} }
if _, replaced, err := tx.Set(user.roleKey(), string(user.Role), nil); err != nil { return merged, nil
return false, err
} else if replaced {
exists = true
}
if _, replaced, err := tx.Set(user.passwordKey(), pwString, nil); err != nil {
return false, err
} else if replaced {
exists = true
}
return exists, nil
} }
func (store *DBStore) AddUser(user User) (err error) { func (store *DBStore) AddUser(user User) (err error) {
@ -153,17 +140,17 @@ func (store *DBStore) AddUser(user User) (err error) {
return ErrInvalidUsername return ErrInvalidUsername
} }
if err = store.db.Update(func(tx *buntdb.Tx) error { if err = store.db.Update(func(tx *buntdb.Tx) error {
if _, exists, err := tx.Set(user.key(), "", nil); err != nil { userBytes, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("cannot marshal user: %w", err)
}
if _, exists, err := tx.Set(user.key(), string(userBytes), nil); err != nil {
return err return err
} else if exists { } else if exists {
return ErrUserExists return ErrUserExists
} }
if exists, err := store.setUserValues(tx, user); err != nil {
return err
} else if exists {
return ErrUserExists
}
return nil return nil
}); err != nil { }); err != nil {
return err return err
@ -175,22 +162,16 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
if strings.Contains(username, ":") { if strings.Contains(username, ":") {
return user, ErrInvalidUsername return user, ErrInvalidUsername
} }
user.Username = username
if err := store.db.View(func(tx *buntdb.Tx) error { if err := store.db.View(func(tx *buntdb.Tx) error {
if val, err := tx.Get(user.roleKey()); err != nil && err != buntdb.ErrNotFound { user.Username = username
if val, err := tx.Get(user.key()); err != nil && err != buntdb.ErrNotFound {
return err return err
} else if err == buntdb.ErrNotFound { } else if err == buntdb.ErrNotFound {
return ErrUserNotFound return ErrUserNotFound
} else { } else if err := json.Unmarshal([]byte(val), &user); err != nil {
user.Role = GlobalRole(val) return fmt.Errorf("cannot unmarshal user: %w", err)
}
if val, err := tx.Get(user.passwordKey()); err != nil && err != buntdb.ErrNotFound {
return err
} else if err == buntdb.ErrNotFound {
return ErrUserNotFound
} else {
user.Password = val
} }
return nil return nil
}); err != nil { }); err != nil {
return user, err return user, err
@ -200,24 +181,26 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
func (store *DBStore) GetUsers() (users []User, err error) { func (store *DBStore) GetUsers() (users []User, err error) {
err = store.db.View(func(tx *buntdb.Tx) error { err = store.db.View(func(tx *buntdb.Tx) error {
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool { var processingError error
if err := tx.AscendKeys(userPrefix+"*", func(key, value string) bool {
var user User var user User
user.Username = strings.TrimPrefix(key, usersPrefix)
if err := json.Unmarshal([]byte(value), &user); err != nil {
processingError = err
return false
}
// Just in case ...
user.Username = strings.TrimPrefix(key, userPrefix)
users = append(users, user) users = append(users, user)
return true return true
}); err != nil { }); err != nil {
return err return err
} }
for i := range users { return processingError
if roleString, err := tx.Get(users[i].roleKey()); err != nil {
return err
} else {
users[i].Role = GlobalRole(roleString)
}
}
return nil
}) })
return users, err return users, err
@ -228,7 +211,29 @@ func (store *DBStore) Update(user User) error {
return ErrInvalidUsername return ErrInvalidUsername
} }
return store.db.Update(func(tx *buntdb.Tx) error { return store.db.Update(func(tx *buntdb.Tx) error {
_, err := store.setUserValues(tx, user) var existingUser User
if val, err := tx.Get(user.key()); err != nil && err != buntdb.ErrNotFound {
return err
} else if err == buntdb.ErrNotFound {
return ErrUserNotFound
} else if err := json.Unmarshal([]byte(val), &existingUser); err != nil {
return fmt.Errorf("cannot unmarshal user: %w", err)
}
mergedUser, err := existingUser.merge(user)
if err != nil {
return fmt.Errorf("cannot merge user: %w", err)
}
mergedUser.Username = user.Username
userBytes, err := json.Marshal(mergedUser)
if err != nil {
return fmt.Errorf("cannot marshal user: %w", err)
}
_, _, err = tx.Set(mergedUser.key(), string(userBytes), nil)
return err return err
}) })
} }
@ -239,35 +244,19 @@ func (store *DBStore) RemoveUser(username string) (err error) {
} }
return store.db.Update(func(tx *buntdb.Tx) error { return store.db.Update(func(tx *buntdb.Tx) error {
user := User{Username: username} user := User{Username: username}
// Delete the main key first. This is a good indicator if the user generally exists.
if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound { if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound {
return ErrUserNotFound return ErrUserNotFound
} else if err != nil { } else if err != nil {
return err return err
} }
// Now get all attributes and delete them as well. One by one.
var keys []string
if err := tx.AscendKeys(user.prefix()+":*", func(key, value string) bool {
keys = append(keys, key)
return true
}); err != nil {
return fmt.Errorf("cannot iterate keys: %w", err)
}
for _, key := range keys {
if _, err := tx.Delete(key); err != nil {
return fmt.Errorf("cannot remove key: %w", err)
}
}
return nil return nil
}) })
} }
func buildPassword(username string, password string) (string, error) { func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0) hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
if err != nil { if err != nil {
return "", err return "", err
} }
return fmt.Sprintf("%s:%s", username, hash), nil return string(hash), nil
} }

View File

@ -31,7 +31,7 @@ func TestStoreUserHandling(t *testing.T) {
}) })
t.Run("adding users should work", func(t *testing.T) { t.Run("adding users should work", func(t *testing.T) {
if err := store.AddUser(User{"myuser", "mypass", GlobalRoleUser}); err != nil { if err := store.AddUser(User{Username: "myuser", Password: "mypass", Role: GlobalRoleUser}); err != nil {
t.Errorf("cannot add user: %v", err) t.Errorf("cannot add user: %v", err)
} }