ShareDAV/store.go

558 lines
15 KiB
Go

// 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 (
"encoding/json"
"errors"
"fmt"
"strings"
uuid "github.com/satori/go.uuid"
"github.com/tidwall/buntdb"
)
type UserStore interface {
AddUser(user User) (err error)
GetUser(username string) (user User, err error)
GetUsers() ([]User, error)
UpdateUser(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)
FindShareByLogin(username, loginName string) (LoginShare, error)
FindSharesByUser(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
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:"
const loginSharePrefix = "loginshare:"
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 != "" {
merged.Password = updates.Password
}
if updates.Role != "" {
merged.Role = updates.Role
}
return merged, nil
}
var ErrShareNotFound = errors.New("share not found")
var ErrLoginNotFound = errors.New("login 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) UpdateUser(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 (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}
var userNames []string
usersPrefix := share.key() + ":user:"
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool {
userNames = append(userNames, strings.TrimPrefix(key, usersPrefix))
return true
}); err != nil {
return err
}
for _, username := range userNames {
if err := store.removeUserFromShare(tx, share, username); err != nil {
return fmt.Errorf("cannot remove user %q from share: %v", username, err)
}
}
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 {
if strings.Contains(username, ":") {
return ErrInvalidUsername
}
return store.db.Update(func(tx *buntdb.Tx) error {
if _, err := tx.Get(share.key()); err != nil {
return ErrShareNotFound
}
if _, _, err := tx.Set(share.key()+":user:"+username, string(role), nil); err != nil {
return fmt.Errorf("cannot set user: %v", err)
}
return nil
})
}
func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error {
var logins []string
loginsPrefix := share.key() + ":login:" + username + ":"
if err := tx.AscendKeys(loginsPrefix+"*", func(key, value string) bool {
logins = append(logins, strings.TrimPrefix(key, loginsPrefix))
return true
}); err != nil {
return err
}
for _, loginName := range logins {
shareIdString, err := tx.Delete(loginSharePrefix + username + ":" + loginName)
if err != nil {
return fmt.Errorf("cannot remove login ref: %v", err)
} else if shareIdString != share.UUID.String() {
return fmt.Errorf("inconsistent login ref %q", username+":"+loginName)
}
if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil {
return fmt.Errorf("cannot remove login %q: %v", loginName, err)
}
}
if _, err := tx.Delete(share.key() + ":user:" + username); err != nil {
return fmt.Errorf("cannot remove user: %v", err)
}
return nil
}
func (store *DBStore) RemoveUserFromShare(share Share, username string) error {
return store.db.Update(func(tx *buntdb.Tx) error {
return store.removeUserFromShare(tx, share, username)
})
}
func (store *DBStore) AddLogin(share Share, username string, login Login) error {
if strings.Contains(username, ":") {
return ErrInvalidUsername
}
return store.db.Update(func(tx *buntdb.Tx) error {
// Validate model. Share should exist and the user should already be assigned.
if _, err := tx.Get(share.key()); err != nil {
return ErrShareNotFound
}
if _, err := tx.Get(share.key() + ":user:" + username); err != nil {
return ErrUserNotFound
}
// Keep a direct reference from username:login to the share, so we can later on
// easily validate login attempts. Also this is an easy approach to check if the
// username:login pair is unique. Otherwise our login scheme wouldn't work the
// way we expect.
// If the existing value happens to be our shareId already, we assume we are just
// updating the existing model. No harm there.
if existingValue, exists, err := tx.Set(loginSharePrefix+username+":"+login.LoginName, share.UUID.String(), nil); err != nil {
return fmt.Errorf("cannot set login reference: %v", err)
} else if exists && existingValue != share.UUID.String() {
return ErrLoginDuplicate
}
// Now simply update the current login information.
if _, _, err := tx.Set(share.key()+":login:"+username+":"+login.LoginName, login.Password, nil); err != nil {
return fmt.Errorf("cannot set login: %v", err)
}
return nil
})
}
func (store *DBStore) RemoveLogin(share Share, username string, loginName string) error {
return store.db.Update(func(tx *buntdb.Tx) error {
if _, err := tx.Get(share.key()); err != nil {
return ErrShareNotFound
}
if shareId, err := tx.Delete(loginSharePrefix + username + ":" + loginName); err != nil {
return ErrLoginNotFound
} else if shareId != share.UUID.String() {
return ErrLoginNotFound
}
if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil {
return ErrLoginNotFound
}
return nil
})
}
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 {
idString := strings.TrimPrefix(key, sharePrefix)
share, err := unmarshalShare(idString, value)
if err != nil {
processingError = err
return false
}
shares = append(shares, share)
return true
}); err != nil {
return err
}
return processingError
})
return shares, err
}
func (store *DBStore) FindShareByLogin(username, loginName string) (loginShare LoginShare, err error) {
err = store.db.View(func(tx *buntdb.Tx) error {
shareIdString, err := tx.Get(loginSharePrefix + username + ":" + loginName)
if err == buntdb.ErrNotFound {
return ErrShareNotFound
} else if err != nil {
return err
}
sharePayload, err := tx.Get(sharePrefix + shareIdString)
if err == buntdb.ErrNotFound {
// TODO warn about inconsistency
return ErrShareNotFound
} else if err != nil {
return err
}
userRole, err := tx.Get(sharePrefix + shareIdString + ":user:" + username)
if err == buntdb.ErrNotFound {
// TODO warn about inconsistency
return ErrShareNotFound
} else if err != nil {
return err
}
loginPassword, err := tx.Get(sharePrefix + shareIdString + ":login:" + username + ":" + loginName)
if err == buntdb.ErrNotFound {
// TODO warn about inconsistency
return ErrShareNotFound
} else if err != nil {
return err
}
share, err := unmarshalShare(shareIdString, sharePayload)
if err != nil {
return err
}
loginShare.Share = share
loginShare.ShareUser = ShareUser{
Username: username,
Role: ShareRole(userRole),
}
loginShare.Login = Login{
LoginName: loginName,
Password: loginPassword,
}
return nil
})
return
}
func (store *DBStore) FindSharesByUser(username string) ([]UserShare, error) {
panic("implement me")
}
func unmarshalShare(idString, payload string) (Share, error) {
var share Share
if err := json.Unmarshal([]byte(payload), &share); err != nil {
return Share{}, err
}
// Just in case ...
if id, err := uuid.FromString(idString); err != nil {
return Share{}, fmt.Errorf("invalid uuid in db: %q: %w", idString, err)
} else {
share.UUID = id
}
return share, nil
}