571 lines
15 KiB
Go
571 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
|
|
ReadOnly bool
|
|
}
|
|
|
|
// 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:"
|
|
const shareuserPrefix = "shareuser:"
|
|
const shareloginPrefix = "sharelogin:"
|
|
|
|
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 (s Share) userKey(username string) string {
|
|
return fmt.Sprintf("%s%s:%s", shareuserPrefix, s.UUID.String(), username)
|
|
}
|
|
|
|
func (s Share) loginKey(username, loginName string) string {
|
|
return fmt.Sprintf("%s%s:%s:%s", shareloginPrefix, s.UUID.String(), username, loginName)
|
|
}
|
|
|
|
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.userKey("")
|
|
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: %w", 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.userKey(username), string(role), nil); err != nil {
|
|
return fmt.Errorf("cannot set user: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error {
|
|
var logins []string
|
|
loginsPrefix := share.loginKey(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: %w", err)
|
|
} else if shareIdString != share.UUID.String() {
|
|
return fmt.Errorf("inconsistent login ref %q", username+":"+loginName)
|
|
}
|
|
|
|
if _, err := tx.Delete(share.loginKey(username, loginName)); err != nil {
|
|
return fmt.Errorf("cannot remove login %q: %w", loginName, err)
|
|
}
|
|
}
|
|
|
|
if _, err := tx.Delete(share.userKey(username)); err != nil {
|
|
return fmt.Errorf("cannot remove user: %w", 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.userKey(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: %w", err)
|
|
} else if exists && existingValue != share.UUID.String() {
|
|
return ErrLoginDuplicate
|
|
}
|
|
|
|
// Now simply update the current login information.
|
|
b, err := json.Marshal(login)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot marshal login: %w", err)
|
|
}
|
|
if _, _, err := tx.Set(share.loginKey(username, login.LoginName), string(b), nil); err != nil {
|
|
return fmt.Errorf("cannot set login: %w", 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.loginKey(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
|
|
}
|
|
|
|
share, err := unmarshalShare(shareIdString, sharePayload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userRole, err := tx.Get(share.userKey(username))
|
|
if err == buntdb.ErrNotFound {
|
|
// TODO warn about inconsistency
|
|
return ErrShareNotFound
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
loginPayload, err := tx.Get(share.loginKey(username, loginName))
|
|
if err == buntdb.ErrNotFound {
|
|
// TODO warn about inconsistency
|
|
return ErrShareNotFound
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
loginShare.Share = share
|
|
loginShare.ShareUser = ShareUser{
|
|
Username: username,
|
|
Role: ShareRole(userRole),
|
|
}
|
|
if err := json.Unmarshal([]byte(loginPayload), &loginShare.Login); err != nil {
|
|
return fmt.Errorf("cannot unmarshal login: %w", err)
|
|
}
|
|
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{}, fmt.Errorf("cannot unmarshal share: %w", 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
|
|
}
|