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 }