// Copyright (c) 2020, Andreas Schneider // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package main import ( "encoding/json" "errors" "fmt" "strings" uuid "github.com/satori/go.uuid" "github.com/tidwall/buntdb" "golang.org/x/crypto/bcrypt" ) type UserStore interface { AddUser(user User) (err error) GetUser(username string) (user User, err error) GetUsers() ([]User, error) Update(user User) error RemoveUser(username string) (err error) } type ShareStore interface { } var _ UserStore = &DBStore{} type GlobalRole string const ( GlobalRoleUser GlobalRole = "user" GlobalRoleAdmin GlobalRole = "admin" ) type ShareRole string const ( ShareRoleReader ShareRole = "reader" ShareRoleWriter ShareRole = "writer" ShareRoleAdmin ShareRole = "admin" ) type User struct { Username string Password string `json:"-"` PasswordHash string `json:"Password"` Role GlobalRole } type Share struct { UUID uuid.UUID Directory string Users []ShareUser } type ShareUser struct { Username string Role ShareRole Logins []Login } type Login struct { Loginname string Password string } const usersPrefix = "users:" type DBStore struct { db *buntdb.DB } func (u User) key() string { return usersPrefix + u.Username } func NewDBStore(filename string) (*DBStore, error) { db, err := buntdb.Open(filename) if err != nil { return nil, err } return &DBStore{db}, nil } func (store *DBStore) Close() error { if err := store.db.Shrink(); err != nil { return err } return store.db.Close() } var ErrUserExists = errors.New("user already exists") var ErrUserNotFound = errors.New("user not found") var ErrInvalidUsername = errors.New("invalid username") func (u User) merge(updates User) (User, error) { merged := u if updates.Password != "" { pwHash, err := hashPassword(updates.Password) if err != nil { return u, fmt.Errorf("cannot hash password: %w", err) } merged.PasswordHash = pwHash } if updates.Role != "" { merged.Role = updates.Role } return merged, nil } func (store *DBStore) AddUser(user User) (err error) { if strings.Contains(user.Username, ":") { return ErrInvalidUsername } if err = store.db.Update(func(tx *buntdb.Tx) error { 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 } else if exists { return ErrUserExists } return nil }); err != nil { return err } return nil } func (store *DBStore) GetUser(username string) (user User, err error) { if strings.Contains(username, ":") { return user, ErrInvalidUsername } if err := store.db.View(func(tx *buntdb.Tx) error { user.Username = username 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), &user); err != nil { return fmt.Errorf("cannot unmarshal user: %w", err) } return nil }); err != nil { return user, err } return user, nil } func (store *DBStore) GetUsers() (users []User, err error) { err = store.db.View(func(tx *buntdb.Tx) error { var processingError error if err := tx.AscendKeys(userPrefix+"*", func(key, value string) bool { var user User 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) return true }); err != nil { return err } return processingError }) return users, err } func (store *DBStore) Update(user User) error { if strings.Contains(user.Username, ":") { return ErrInvalidUsername } return store.db.Update(func(tx *buntdb.Tx) error { 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 }) } func (store *DBStore) RemoveUser(username string) (err error) { if strings.Contains(username, ":") { return ErrInvalidUsername } return store.db.Update(func(tx *buntdb.Tx) error { user := User{Username: username} if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound { return ErrUserNotFound } else if err != nil { return err } return nil }) } func hashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), 0) if err != nil { return "", err } return string(hash), nil }