⬆️ update dependencies, ✨ implement user store
This commit is contained in:
@@ -1,106 +1,166 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/tidwall/buntdb"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserStore interface {
|
||||
AddUser(username string, role GlobalRole, password string) (err error)
|
||||
AddUser(user User) (err error)
|
||||
GetUser(username string) (user User, err error)
|
||||
GetLogin(username string, password string) (appName string, err error)
|
||||
SetLogin(username string, appName string, password string) (err error)
|
||||
RemoveLogin(username string, appName string) (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"
|
||||
GlobalRoleManager GlobalRole = "manager"
|
||||
GlobalRoleUser GlobalRole = "user"
|
||||
GlobalRoleAdmin GlobalRole = "admin"
|
||||
)
|
||||
|
||||
type ShareRole string
|
||||
|
||||
const (
|
||||
ShareRoleReader ShareRole = "reader"
|
||||
ShareRoleWriter ShareRole = "writer"
|
||||
ShareRoleAdmin ShareRole = "admin"
|
||||
ShareRoleAdmin ShareRole = "admin"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Role GlobalRole
|
||||
Apps []App
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
Password string
|
||||
Role GlobalRole
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
UUID uuid.UUID
|
||||
Directory string
|
||||
Users []ShareUser
|
||||
Users []ShareUser
|
||||
}
|
||||
|
||||
type ShareUser struct {
|
||||
Username string
|
||||
Role ShareRole
|
||||
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 (u User) prefix() string {
|
||||
return fmt.Sprintf("user:%s", u.Username)
|
||||
}
|
||||
|
||||
func (u User) roleKey() string {
|
||||
return u.prefix() + ":role"
|
||||
}
|
||||
|
||||
func (u User) passwordKey() string {
|
||||
return u.prefix() + ":password"
|
||||
}
|
||||
|
||||
func NewDBStore(filename string) (*DBStore, error) {
|
||||
db, err := buntdb.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.CreateIndex("login", "user:*:login*", buntdb.IndexBinary); err != nil && err != buntdb.ErrIndexExists {
|
||||
return nil, errors.New("cannot create login index: " + err.Error())
|
||||
}
|
||||
|
||||
return &DBStore{db}, nil
|
||||
}
|
||||
|
||||
func (store *DBStore) Close() error {
|
||||
return store.db.Close()
|
||||
}
|
||||
|
||||
var ErrExists = errors.New("key already exists")
|
||||
var ErrUserNotFound = errors.New("user not found")
|
||||
var ErrInvalidUsername = errors.New("invalid username")
|
||||
|
||||
func (store *DBStore) AddUser(username string, role GlobalRole, password string) (err error) {
|
||||
if strings.Contains(username, ":") {
|
||||
func (store *DBStore) setUserValues(tx *buntdb.Tx, user User) (exists bool, err error) {
|
||||
pwString, err := buildPassword(user.Username, user.Password)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot hash password: %w", err)
|
||||
}
|
||||
|
||||
if _, replaced, err := tx.Set(user.roleKey(), string(user.Role), nil); err != nil {
|
||||
return false, err
|
||||
} else if replaced {
|
||||
exists = true
|
||||
}
|
||||
if _, replaced, err := tx.Set(user.passwordKey(), pwString, nil); err != nil {
|
||||
return false, err
|
||||
} else if replaced {
|
||||
exists = true
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (store *DBStore) AddUser(user User) (err error) {
|
||||
if strings.Contains(user.Username, ":") {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
pwString, err := buildPassword(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = store.db.Update(func(tx *buntdb.Tx) error {
|
||||
if _, replaced, err := tx.Set(fmt.Sprintf("user:%s:role", username), string(role), nil); err != nil {
|
||||
if _, exists, err := tx.Set(user.key(), "", nil); err != nil {
|
||||
return err
|
||||
} else if replaced {
|
||||
} else if exists {
|
||||
return ErrExists
|
||||
}
|
||||
if _, replaced, err := tx.Set(fmt.Sprintf("user:%s:login", username), pwString, nil); err != nil {
|
||||
return err
|
||||
} else if replaced {
|
||||
return ErrExists
|
||||
}
|
||||
// just in case ...
|
||||
if _, err := tx.Delete(fmt.Sprintf("user:%s:login:*", username)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists, err := store.setUserValues(tx, user); err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return ErrExists
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -114,33 +174,20 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
|
||||
}
|
||||
user.Username = username
|
||||
if err := store.db.View(func(tx *buntdb.Tx) error {
|
||||
if val, err := tx.Get(fmt.Sprintf("user:%s:role", username)); err != nil && err != buntdb.ErrNotFound {
|
||||
if val, err := tx.Get(user.roleKey()); err != nil && err != buntdb.ErrNotFound {
|
||||
return err
|
||||
} else if err == buntdb.ErrNotFound {
|
||||
return ErrUserNotFound
|
||||
} else {
|
||||
user.Role = GlobalRole(val)
|
||||
}
|
||||
|
||||
loginPrefix := fmt.Sprintf("user:%s:login:", username)
|
||||
if err := tx.AscendKeys(loginPrefix + "*", func(key, value string) bool {
|
||||
app := App{Name: strings.TrimPrefix(key, loginPrefix)}
|
||||
creationTimeValue, err := tx.Get(fmt.Sprintf("user:%s:app:%s:created_at", username, app.Name))
|
||||
if err != nil {
|
||||
// invalid app ... move along
|
||||
return true
|
||||
}
|
||||
creationTime, err := time.Parse(time.RFC3339Nano, creationTimeValue)
|
||||
if err != nil {
|
||||
// invalid creation time ... invalid app ... move along.
|
||||
return true
|
||||
}
|
||||
app.CreatedAt = creationTime
|
||||
user.Apps = append(user.Apps, app)
|
||||
|
||||
return true
|
||||
}); err != nil {
|
||||
if val, err := tx.Get(user.passwordKey()); err != nil && err != buntdb.ErrNotFound {
|
||||
return err
|
||||
} else if err == buntdb.ErrNotFound {
|
||||
return ErrUserNotFound
|
||||
} else {
|
||||
user.Password = val
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return user, err
|
||||
@@ -148,56 +195,39 @@ func (store *DBStore) GetUser(username string) (user User, err error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *DBStore) GetLogin(username string, password string) (appName string, err error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (store *DBStore) SetLogin(username string, appName string, password string) (err error) {
|
||||
if strings.Contains(username, ":") {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
|
||||
pwString, err := buildPassword(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||
mainLoginKey := fmt.Sprintf("user:%s:login", username)
|
||||
// check if we actually know the user. the current login is a good indicator
|
||||
if _, err := tx.Get(mainLoginKey); err != nil {
|
||||
if err == buntdb.ErrNotFound {
|
||||
return ErrUserNotFound
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
func (store *DBStore) GetUsers() (users []User, err error) {
|
||||
err = store.db.View(func(tx *buntdb.Tx) error {
|
||||
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool {
|
||||
var user User
|
||||
user.Username = strings.TrimPrefix(key, usersPrefix)
|
||||
users = append(users, user)
|
||||
return true
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saneAppName := strings.TrimSpace(appName)
|
||||
|
||||
// if no appname is given, we apparently have to update the password of the whole account.
|
||||
if saneAppName == "" {
|
||||
if _, _, err := tx.Set(mainLoginKey, pwString, nil); err != nil {
|
||||
for i := range users {
|
||||
if roleString, err := tx.Get(users[i].roleKey()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, replaced, err := tx.Set(fmt.Sprintf("%s:%s", mainLoginKey, saneAppName), pwString, nil); err != nil {
|
||||
return err
|
||||
} else if !replaced {
|
||||
// o,h so this is a new key. set the timestamp
|
||||
creationTime := time.Now().Format(time.RFC3339Nano)
|
||||
if _, _, err := tx.Set(fmt.Sprintf("user:%s:app:%s:created_at", username, saneAppName), creationTime, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
users[i].Role = GlobalRole(roleString)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (store *DBStore) RemoveLogin(username string, appName string) (err error) {
|
||||
panic("implement me")
|
||||
func (store *DBStore) Update(user User) error {
|
||||
if strings.Contains(user.Username, ":") {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||
_, err := store.setUserValues(tx, user)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (store *DBStore) RemoveUser(username string) (err error) {
|
||||
@@ -205,9 +235,29 @@ func (store *DBStore) RemoveUser(username string) (err error) {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||
// TODO won't work ... collect keys first, then delete them one by one
|
||||
_, err := tx.Delete(fmt.Sprintf("user:%s:*", username))
|
||||
return err
|
||||
user := User{Username: username}
|
||||
|
||||
// Delete the main key first. This is a good indicator if the user generally exists.
|
||||
if _, err := tx.Delete(user.key()); err == buntdb.ErrNotFound {
|
||||
return ErrUserNotFound
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now get all attributes and delete them as well. One by one.
|
||||
var keys []string
|
||||
if err := tx.AscendKeys(user.prefix()+":*", func(key, value string) bool {
|
||||
keys = append(keys, key)
|
||||
return true
|
||||
}); err != nil {
|
||||
return fmt.Errorf("cannot iterate keys: %w", err)
|
||||
}
|
||||
for _, key := range keys {
|
||||
if _, err := tx.Delete(key); err != nil {
|
||||
return fmt.Errorf("cannot remove key: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -217,4 +267,4 @@ func buildPassword(username string, password string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", username, hash), nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user