// 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 { CreateShare() (Share, error) UpdateShareAttributes(share Share) error RemoveShare(id uuid.UUID) error AddUserToShare(share Share, username string, role ShareRole) error RemoveUserFromShare(share Share, username string) error AddLogin(share Share, username string, login Login) error RemoveLogin(share Share, username string, loginName string) error GetShares() ([]Share, error) FindByLogin(username, loginName string) (LoginShare, error) FindByUser(username string) ([]UserShare, error) } var _ UserStore = (*DBStore)(nil) var _ ShareStore = (*DBStore)(nil) 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 Name string Description string } type ShareUser struct { Username string Role ShareRole } type Login struct { LoginName string Password string } // View on a share from the perspective of a user of that share. type UserShare struct { Share Role ShareRole } // View on a share from the perspective of a specific login (with associated user). type LoginShare struct { Share ShareUser Login } const userPrefix = "user:" const sharePrefix = "share:" type DBStore struct { db *buntdb.DB } func (u User) key() string { return userPrefix + u.Username } func (s Share) key() string { return sharePrefix + s.UUID.String() } 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 } var ErrShareNotFound = errors.New("share not found") var ErrLoginDuplicate = errors.New("login already exists") 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 } func (store *DBStore) CreateShare() (Share, error) { share := Share{ UUID: uuid.NewV4(), } if err := store.db.Update(func(tx *buntdb.Tx) error { b, err := json.Marshal(share) if err != nil { return fmt.Errorf("cannot marshal share: %w", err) } if _, _, err := tx.Set(share.key(), string(b), nil); err != nil { return err } return nil }); err != nil { return Share{}, err } return share, nil } func (store *DBStore) UpdateShareAttributes(share Share) error { return store.db.Update(func(tx *buntdb.Tx) error { var existingShare Share if val, err := tx.Get(share.key()); err == buntdb.ErrNotFound { return ErrShareNotFound } else if err != nil { return err } else if err := json.Unmarshal([]byte(val), &existingShare); err != nil { return fmt.Errorf("cannot unmarshal share %s: %w", share.UUID, err) } existingShare.UUID = share.UUID existingShare.Name = share.Name existingShare.Description = share.Description if b, err := json.Marshal(existingShare); err != nil { return fmt.Errorf("cannot marshal share %s: %w", share.UUID, err) } else if _, _, err := tx.Set(existingShare.key(), string(b), nil); err != nil { return err } return nil }) } func (store *DBStore) RemoveShare(id uuid.UUID) error { return store.db.Update(func(tx *buntdb.Tx) error { share := Share{UUID: id} if _, err := tx.Delete(share.key()); err == buntdb.ErrNotFound { return ErrShareNotFound } else { return err } }) } func (store *DBStore) AddUserToShare(share Share, username string, role ShareRole) error { panic("implement me") } func (store *DBStore) RemoveUserFromShare(share Share, username string) error { panic("implement me") } func (store *DBStore) AddLogin(share Share, username string, login Login) error { panic("implement me") } func (store *DBStore) RemoveLogin(share Share, username string, loginName string) error { panic("implement me") } func (store *DBStore) GetShares() (shares []Share, err error) { err = store.db.View(func(tx *buntdb.Tx) error { var processingError error if err := tx.AscendKeys(sharePrefix+"*", func(key, value string) bool { var share Share if err := json.Unmarshal([]byte(value), &share); err != nil { processingError = err return false } // Just in case ... idString := strings.TrimPrefix(key, sharePrefix) if id, err := uuid.FromString(idString); err != nil { processingError = fmt.Errorf("invalid uuid in db: %q: %w", idString, err) return false } else { share.UUID = id } shares = append(shares, share) return true }); err != nil { return err } return processingError }) return shares, err } func (store *DBStore) FindByLogin(username, loginName string) (LoginShare, error) { panic("implement me") } func (store *DBStore) FindByUser(username string) ([]UserShare, error) { panic("implement me") }