diff --git a/store.go b/store.go new file mode 100644 index 0000000..b419912 --- /dev/null +++ b/store.go @@ -0,0 +1,220 @@ +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 +} \ No newline at end of file