// 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 ( "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 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 (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) { 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 (store *DBStore) setUserValues(tx *buntdb.Tx, user User) (exists bool, err error) { pwString, err := buildPassword(user.Username, user.Password) if err != nil { return false, fmt.Errorf("cannot hash password: %w", err) } if _, replaced, err := tx.Set(user.roleKey(), string(user.Role), nil); err != 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) { if strings.Contains(user.Username, ":") { return ErrInvalidUsername } if err = store.db.Update(func(tx *buntdb.Tx) error { if _, exists, err := tx.Set(user.key(), "", nil); err != nil { return err } else if exists { return ErrUserExists } if exists, err := store.setUserValues(tx, user); 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 } user.Username = username if err := store.db.View(func(tx *buntdb.Tx) error { if val, err := tx.Get(user.roleKey()); err != nil && err != buntdb.ErrNotFound { return err } else if err == buntdb.ErrNotFound { return ErrUserNotFound } else { user.Role = GlobalRole(val) } 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 }); 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 { if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool { var user User user.Username = strings.TrimPrefix(key, usersPrefix) users = append(users, user) return true }); err != nil { return err } for i := range users { if roleString, err := tx.Get(users[i].roleKey()); err != nil { return err } else { users[i].Role = GlobalRole(roleString) } } return nil }) 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 { _, err := store.setUserValues(tx, user) 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} // Delete the main key first. This is a good indicator if the user generally exists. if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound { return ErrUserNotFound } else if err != nil { 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 }) } func buildPassword(username string, password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), 0) if err != nil { return "", err } return fmt.Sprintf("%s:%s", username, hash), nil }