220 lines
5.5 KiB
Go
220 lines
5.5 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"github.com/tidwall/buntdb"
|
||
|
"golang.org/x/crypto/bcrypt"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type UserStore interface {
|
||
|
AddUser(username string, role GlobalRole, password string) (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)
|
||
|
RemoveUser(username string) (err error)
|
||
|
}
|
||
|
|
||
|
type ShareStore interface {
|
||
|
|
||
|
}
|
||
|
|
||
|
type GlobalRole string
|
||
|
const (
|
||
|
GlobalRoleUser GlobalRole = "user"
|
||
|
GlobalRoleManager GlobalRole = "manager"
|
||
|
GlobalRoleAdmin GlobalRole = "admin"
|
||
|
)
|
||
|
|
||
|
type ShareRole string
|
||
|
const (
|
||
|
ShareRoleReader ShareRole = "reader"
|
||
|
ShareRoleWriter ShareRole = "writer"
|
||
|
ShareRoleAdmin ShareRole = "admin"
|
||
|
)
|
||
|
|
||
|
type User struct {
|
||
|
Username string
|
||
|
Role GlobalRole
|
||
|
Apps []App
|
||
|
}
|
||
|
|
||
|
type App struct {
|
||
|
Name string
|
||
|
CreatedAt time.Time
|
||
|
}
|
||
|
|
||
|
type Share struct {
|
||
|
Directory string
|
||
|
Users []ShareUser
|
||
|
}
|
||
|
|
||
|
type ShareUser struct {
|
||
|
Username string
|
||
|
Role ShareRole
|
||
|
}
|
||
|
|
||
|
type DBStore struct {
|
||
|
db *buntdb.DB
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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, ":") {
|
||
|
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 {
|
||
|
return err
|
||
|
} else if replaced {
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
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 {
|
||
|
return err
|
||
|
} 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 {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}); err != nil {
|
||
|
return user, err
|
||
|
}
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 {
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (store *DBStore) RemoveLogin(username string, appName string) (err error) {
|
||
|
panic("implement me")
|
||
|
}
|
||
|
|
||
|
func (store *DBStore) RemoveUser(username string) (err error) {
|
||
|
if strings.Contains(username, ":") {
|
||
|
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
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func buildPassword(username string, password string) (string, error) {
|
||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return fmt.Sprintf("%s:%s", username, hash), nil
|
||
|
}
|