ShareDAV/store.go

410 lines
10 KiB
Go
Raw Normal View History

// 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 <organization> 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 <COPYRIGHT HOLDER> 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 (
2020-10-10 19:06:49 +02:00
"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 {
2020-10-12 19:51:44 +02:00
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)
}
2020-10-12 19:51:44 +02:00
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 {
2020-10-10 19:06:49 +02:00
Username string
Password string `json:"-"`
PasswordHash string `json:"Password"`
Role GlobalRole
}
type Share struct {
2020-10-12 19:51:44 +02:00
UUID uuid.UUID
Name string
Description string
}
type ShareUser struct {
Username string
Role ShareRole
}
type Login struct {
2020-10-12 19:51:44 +02:00
LoginName string
Password string
}
2020-10-12 19:51:44 +02:00
// 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 {
2020-10-12 19:51:44 +02:00
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 {
2020-10-10 16:29:26 +02:00
if err := store.db.Shrink(); err != nil {
return err
}
return store.db.Close()
}
2020-10-10 16:30:35 +02:00
var ErrUserExists = errors.New("user already exists")
var ErrUserNotFound = errors.New("user not found")
var ErrInvalidUsername = errors.New("invalid username")
2020-10-10 19:06:49 +02:00
func (u User) merge(updates User) (User, error) {
merged := u
2020-10-10 19:06:49 +02:00
if updates.Password != "" {
pwHash, err := hashPassword(updates.Password)
if err != nil {
return u, fmt.Errorf("cannot hash password: %w", err)
}
merged.PasswordHash = pwHash
}
2020-10-10 19:06:49 +02:00
if updates.Role != "" {
merged.Role = updates.Role
}
2020-10-10 19:06:49 +02:00
return merged, nil
}
2020-10-12 19:51:44 +02:00
var ErrShareNotFound = errors.New("share not found")
2020-10-13 19:59:35 +02:00
var ErrLoginDuplicate = errors.New("login already exists")
2020-10-12 19:51:44 +02:00
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 {
2020-10-10 19:06:49 +02:00
userBytes, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("cannot marshal user: %w", err)
}
2020-10-10 19:06:49 +02:00
if _, exists, err := tx.Set(user.key(), string(userBytes), nil); err != nil {
return err
} else if exists {
2020-10-10 16:30:35 +02:00
return ErrUserExists
}
2020-10-10 19:06:49 +02:00
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 {
2020-10-10 19:06:49 +02:00
user.Username = username
if val, err := tx.Get(user.key()); err != nil && err != buntdb.ErrNotFound {
return err
} else if err == buntdb.ErrNotFound {
return ErrUserNotFound
2020-10-10 19:06:49 +02:00
} else if err := json.Unmarshal([]byte(val), &user); err != nil {
return fmt.Errorf("cannot unmarshal user: %w", err)
}
2020-10-10 19:06:49 +02:00
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 {
2020-10-10 19:06:49 +02:00
var processingError error
if err := tx.AscendKeys(userPrefix+"*", func(key, value string) bool {
var user User
2020-10-10 19:06:49 +02:00
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
}
2020-10-10 19:06:49 +02:00
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 {
2020-10-10 19:06:49 +02:00
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
})
}
2020-10-10 19:06:49 +02:00
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
if err != nil {
return "", err
}
2020-10-10 19:06:49 +02:00
return string(hash), nil
}
2020-10-12 19:51:44 +02:00
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")
}