ShareDAV/store.go

263 lines
6.4 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"
"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 `json:"-"`
PasswordHash string `json:"Password"`
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 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
}
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
}