diff --git a/cmd_user.go b/cmd_user.go index 4b628f5..a23ad75 100644 --- a/cmd_user.go +++ b/cmd_user.go @@ -119,7 +119,7 @@ func (cmd CmdUserUpdate) Run(app *app) error { return nil } - return app.userStore.Update(user) + return app.userStore.UpdateUser(user) } type CmdUserDelete struct { diff --git a/store.go b/store.go index 86d18cf..43351fb 100644 --- a/store.go +++ b/store.go @@ -40,7 +40,7 @@ type UserStore interface { AddUser(user User) (err error) GetUser(username string) (user User, err error) GetUsers() ([]User, error) - Update(user User) error + UpdateUser(user User) error RemoveUser(username string) (err error) } @@ -55,8 +55,8 @@ type ShareStore interface { GetShares() ([]Share, error) - FindByLogin(username, loginName string) (LoginShare, error) - FindByUser(username string) ([]UserShare, error) + FindShareByLogin(username, loginName string) (LoginShare, error) + FindSharesByUser(username string) ([]UserShare, error) } var _ UserStore = (*DBStore)(nil) @@ -115,6 +115,7 @@ type LoginShare struct { const userPrefix = "user:" const sharePrefix = "share:" +const loginSharePrefix = "loginshare:" type DBStore struct { db *buntdb.DB @@ -166,6 +167,7 @@ func (u User) merge(updates User) (User, error) { } var ErrShareNotFound = errors.New("share not found") +var ErrLoginNotFound = errors.New("login not found") var ErrLoginDuplicate = errors.New("login already exists") func (store *DBStore) AddUser(user User) (err error) { @@ -239,7 +241,7 @@ func (store *DBStore) GetUsers() (users []User, err error) { return users, err } -func (store *DBStore) Update(user User) error { +func (store *DBStore) UpdateUser(user User) error { if strings.Contains(user.Username, ":") { return ErrInvalidUsername } @@ -343,6 +345,22 @@ func (store *DBStore) UpdateShareAttributes(share Share) error { func (store *DBStore) RemoveShare(id uuid.UUID) error { return store.db.Update(func(tx *buntdb.Tx) error { share := Share{UUID: id} + + var userNames []string + usersPrefix := share.key() + ":user:" + if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool { + userNames = append(userNames, strings.TrimPrefix(key, usersPrefix)) + return true + }); err != nil { + return err + } + + for _, username := range userNames { + if err := store.removeUserFromShare(tx, share, username); err != nil { + return fmt.Errorf("cannot remove user %q from share: %v", username, err) + } + } + if _, err := tx.Delete(share.key()); err == buntdb.ErrNotFound { return ErrShareNotFound } else { @@ -352,19 +370,107 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error { } func (store *DBStore) AddUserToShare(share Share, username string, role ShareRole) error { - panic("implement me") + if strings.Contains(username, ":") { + return ErrInvalidUsername + } + return store.db.Update(func(tx *buntdb.Tx) error { + if _, err := tx.Get(share.key()); err != nil { + return ErrShareNotFound + } + if _, _, err := tx.Set(share.key()+":user:"+username, string(role), nil); err != nil { + return fmt.Errorf("cannot set user: %v", err) + } + return nil + }) +} + +func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error { + var logins []string + loginsPrefix := share.key() + ":login:" + username + ":" + if err := tx.AscendKeys(loginsPrefix+"*", func(key, value string) bool { + logins = append(logins, strings.TrimPrefix(key, loginsPrefix)) + return true + }); err != nil { + return err + } + + for _, loginName := range logins { + shareIdString, err := tx.Delete(loginSharePrefix + username + ":" + loginName) + if err != nil { + return fmt.Errorf("cannot remove login ref: %v", err) + } else if shareIdString != share.UUID.String() { + return fmt.Errorf("inconsistent login ref %q", username+":"+loginName) + } + + if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil { + return fmt.Errorf("cannot remove login %q: %v", loginName, err) + } + } + + if _, err := tx.Delete(share.key() + ":user:" + username); err != nil { + return fmt.Errorf("cannot remove user: %v", err) + } + + return nil } func (store *DBStore) RemoveUserFromShare(share Share, username string) error { - panic("implement me") + return store.db.Update(func(tx *buntdb.Tx) error { + return store.removeUserFromShare(tx, share, username) + }) } func (store *DBStore) AddLogin(share Share, username string, login Login) error { - panic("implement me") + if strings.Contains(username, ":") { + return ErrInvalidUsername + } + return store.db.Update(func(tx *buntdb.Tx) error { + // Validate model. Share should exist and the user should already be assigned. + if _, err := tx.Get(share.key()); err != nil { + return ErrShareNotFound + } + if _, err := tx.Get(share.key() + ":user:" + username); err != nil { + return ErrUserNotFound + } + + // Keep a direct reference from username:login to the share, so we can later on + // easily validate login attempts. Also this is an easy approach to check if the + // username:login pair is unique. Otherwise our login scheme wouldn't work the + // way we expect. + // If the existing value happens to be our shareId already, we assume we are just + // updating the existing model. No harm there. + if existingValue, exists, err := tx.Set(loginSharePrefix+username+":"+login.LoginName, share.UUID.String(), nil); err != nil { + return fmt.Errorf("cannot set login reference: %v", err) + } else if exists && existingValue != share.UUID.String() { + return ErrLoginDuplicate + } + + // Now simply update the current login information. + if _, _, err := tx.Set(share.key()+":login:"+username+":"+login.LoginName, login.Password, nil); err != nil { + return fmt.Errorf("cannot set login: %v", err) + } + return nil + }) } func (store *DBStore) RemoveLogin(share Share, username string, loginName string) error { - panic("implement me") + return store.db.Update(func(tx *buntdb.Tx) error { + if _, err := tx.Get(share.key()); err != nil { + return ErrShareNotFound + } + + if shareId, err := tx.Delete(loginSharePrefix + username + ":" + loginName); err != nil { + return ErrLoginNotFound + } else if shareId != share.UUID.String() { + return ErrLoginNotFound + } + + if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil { + return ErrLoginNotFound + } + + return nil + }) } func (store *DBStore) GetShares() (shares []Share, err error) { @@ -372,22 +478,12 @@ func (store *DBStore) GetShares() (shares []Share, err error) { var processingError error if err := tx.AscendKeys(sharePrefix+"*", func(key, value string) bool { - var share Share - - if err := json.Unmarshal([]byte(value), &share); err != nil { + idString := strings.TrimPrefix(key, sharePrefix) + share, err := unmarshalShare(idString, value) + if err != nil { processingError = err return false } - - // Just in case ... - idString := strings.TrimPrefix(key, sharePrefix) - if id, err := uuid.FromString(idString); err != nil { - processingError = fmt.Errorf("invalid uuid in db: %q: %w", idString, err) - return false - } else { - share.UUID = id - } - shares = append(shares, share) return true }); err != nil { @@ -400,10 +496,76 @@ func (store *DBStore) GetShares() (shares []Share, err error) { return shares, err } -func (store *DBStore) FindByLogin(username, loginName string) (LoginShare, error) { +func (store *DBStore) FindShareByLogin(username, loginName string) (loginShare LoginShare, err error) { + err = store.db.View(func(tx *buntdb.Tx) error { + shareIdString, err := tx.Get(loginSharePrefix + username + ":" + loginName) + if err == buntdb.ErrNotFound { + return ErrShareNotFound + } else if err != nil { + return err + } + + sharePayload, err := tx.Get(sharePrefix + shareIdString) + if err == buntdb.ErrNotFound { + // TODO warn about inconsistency + return ErrShareNotFound + } else if err != nil { + return err + } + + userRole, err := tx.Get(sharePrefix + shareIdString + ":user:" + username) + if err == buntdb.ErrNotFound { + // TODO warn about inconsistency + return ErrShareNotFound + } else if err != nil { + return err + } + + loginPassword, err := tx.Get(sharePrefix + shareIdString + ":login:" + username + ":" + loginName) + if err == buntdb.ErrNotFound { + // TODO warn about inconsistency + return ErrShareNotFound + } else if err != nil { + return err + } + + share, err := unmarshalShare(shareIdString, sharePayload) + if err != nil { + return err + } + + loginShare.Share = share + loginShare.ShareUser = ShareUser{ + Username: username, + Role: ShareRole(userRole), + } + loginShare.Login = Login{ + LoginName: loginName, + Password: loginPassword, + } + + return nil + }) + + return +} + +func (store *DBStore) FindSharesByUser(username string) ([]UserShare, error) { panic("implement me") } -func (store *DBStore) FindByUser(username string) ([]UserShare, error) { - panic("implement me") +func unmarshalShare(idString, payload string) (Share, error) { + var share Share + if err := json.Unmarshal([]byte(payload), &share); err != nil { + return Share{}, err + } + + // Just in case ... + if id, err := uuid.FromString(idString); err != nil { + return Share{}, fmt.Errorf("invalid uuid in db: %q: %w", idString, err) + } else { + share.UUID = id + } + + return share, nil } diff --git a/store_test.go b/store_test.go index fecf480..7aae93e 100644 --- a/store_test.go +++ b/store_test.go @@ -196,6 +196,11 @@ func TestStoreShareHandling(t *testing.T) { _ = store.AddUser(user1) _ = store.AddUser(user2) + defer func() { + _ = store.RemoveUser(user1.Username) + _ = store.RemoveUser(user2.Username) + }() + t.Run("multiple shares should exist", func(t *testing.T) { shares, _ := store.GetShares() if len(shares) != 3 { @@ -238,23 +243,25 @@ func TestStoreShareHandling(t *testing.T) { } t.Run("duplicate login not allowed", func(t *testing.T) { - if err := store.AddLogin(share1, user2.Username, login1); err != ErrLoginDuplicate { + // Different share, but same user:login pair as above. Must be a share where + // that user is already assigned, though. + if err := store.AddLogin(share2, user2.Username, login2); err != ErrLoginDuplicate { t.Errorf("unexpected error: %v", err) } }) t.Run("share is found by login", func(t *testing.T) { - share, err := store.FindByLogin(user1.Username, login1.LoginName) + share, err := store.FindShareByLogin(user1.Username, login1.LoginName) if err != nil { t.Errorf("unexpected error: %v", err) } if share.UUID != share1.UUID { - t.Errorf("wrong store returned") + t.Errorf("wrong share returned") } }) t.Run("unknown login/share combination returns error", func(t *testing.T) { - if _, err := store.FindByLogin(user1.Username, login3.LoginName); err != ErrShareNotFound { + if _, err := store.FindShareByLogin(user1.Username, login3.LoginName); err != ErrShareNotFound { t.Errorf("unexpected error: %v", err) } }) @@ -263,29 +270,41 @@ func TestStoreShareHandling(t *testing.T) { if err := store.RemoveLogin(share1, user1.Username, login1.LoginName); err != nil { t.Errorf("unexpected error: %v", err) } - if _, err := store.FindByLogin(user1.Username, login1.LoginName); err != ErrShareNotFound { + if _, err := store.FindShareByLogin(user1.Username, login1.LoginName); err != ErrShareNotFound { t.Errorf("share should not be found now, but returned: %v", err) } }) t.Run("user can be removed", func(t *testing.T) { - if err := store.RemoveUserFromShare(share2, user2.Username); err != nil { + if err := store.RemoveUserFromShare(share1, user2.Username); err != nil { t.Errorf("unexpected error: %v", err) } - if _, err := store.FindByLogin(user2.Username, login2.LoginName); err != ErrShareNotFound { + if _, err := store.FindShareByLogin(user2.Username, login2.LoginName); err != ErrShareNotFound { t.Errorf("share should not be found now, but returned: %v", err) } }) }) }) + + t.Run("can remove shares", func(t *testing.T) { + if err := store.RemoveShare(share1.UUID); err != nil { + t.Errorf("cannot remove share1: %v", err) + } + if err := store.RemoveShare(share2.UUID); err != nil { + t.Errorf("cannot remove share2: %v", err) + } + if err := store.RemoveShare(share3.UUID); err != nil { + t.Errorf("cannot remove share3: %v", err) + } + }) }) t.Run("database should be empty now", func(t *testing.T) { // checks that we properly deleted all keys if err := store.db.View(func(tx *buntdb.Tx) error { return tx.Ascend("", func(key, value string) bool { - t.Errorf("there should be no keys left") - return false + t.Errorf("leftover key found: %v", key) + return true }) }); err != nil { t.Errorf("iterating keys failed: %v", err)