✨ Implement share CRUD operations
This commit is contained in:
parent
3d4f6f681c
commit
db5fb05ce1
|
@ -119,7 +119,7 @@ func (cmd CmdUserUpdate) Run(app *app) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.userStore.Update(user)
|
return app.userStore.UpdateUser(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CmdUserDelete struct {
|
type CmdUserDelete struct {
|
||||||
|
|
210
store.go
210
store.go
|
@ -40,7 +40,7 @@ type UserStore interface {
|
||||||
AddUser(user User) (err error)
|
AddUser(user User) (err error)
|
||||||
GetUser(username string) (user User, err error)
|
GetUser(username string) (user User, err error)
|
||||||
GetUsers() ([]User, error)
|
GetUsers() ([]User, error)
|
||||||
Update(user User) error
|
UpdateUser(user User) error
|
||||||
RemoveUser(username string) (err error)
|
RemoveUser(username string) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +55,8 @@ type ShareStore interface {
|
||||||
|
|
||||||
GetShares() ([]Share, error)
|
GetShares() ([]Share, error)
|
||||||
|
|
||||||
FindByLogin(username, loginName string) (LoginShare, error)
|
FindShareByLogin(username, loginName string) (LoginShare, error)
|
||||||
FindByUser(username string) ([]UserShare, error)
|
FindSharesByUser(username string) ([]UserShare, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ UserStore = (*DBStore)(nil)
|
var _ UserStore = (*DBStore)(nil)
|
||||||
|
@ -115,6 +115,7 @@ type LoginShare struct {
|
||||||
|
|
||||||
const userPrefix = "user:"
|
const userPrefix = "user:"
|
||||||
const sharePrefix = "share:"
|
const sharePrefix = "share:"
|
||||||
|
const loginSharePrefix = "loginshare:"
|
||||||
|
|
||||||
type DBStore struct {
|
type DBStore struct {
|
||||||
db *buntdb.DB
|
db *buntdb.DB
|
||||||
|
@ -166,6 +167,7 @@ func (u User) merge(updates User) (User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrShareNotFound = errors.New("share not found")
|
var ErrShareNotFound = errors.New("share not found")
|
||||||
|
var ErrLoginNotFound = errors.New("login not found")
|
||||||
var ErrLoginDuplicate = errors.New("login already exists")
|
var ErrLoginDuplicate = errors.New("login already exists")
|
||||||
|
|
||||||
func (store *DBStore) AddUser(user User) (err error) {
|
func (store *DBStore) AddUser(user User) (err error) {
|
||||||
|
@ -239,7 +241,7 @@ func (store *DBStore) GetUsers() (users []User, err error) {
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *DBStore) Update(user User) error {
|
func (store *DBStore) UpdateUser(user User) error {
|
||||||
if strings.Contains(user.Username, ":") {
|
if strings.Contains(user.Username, ":") {
|
||||||
return ErrInvalidUsername
|
return ErrInvalidUsername
|
||||||
}
|
}
|
||||||
|
@ -343,6 +345,22 @@ func (store *DBStore) UpdateShareAttributes(share Share) error {
|
||||||
func (store *DBStore) RemoveShare(id uuid.UUID) error {
|
func (store *DBStore) RemoveShare(id uuid.UUID) error {
|
||||||
return store.db.Update(func(tx *buntdb.Tx) error {
|
return store.db.Update(func(tx *buntdb.Tx) error {
|
||||||
share := Share{UUID: id}
|
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 {
|
if _, err := tx.Delete(share.key()); err == buntdb.ErrNotFound {
|
||||||
return ErrShareNotFound
|
return ErrShareNotFound
|
||||||
} else {
|
} else {
|
||||||
|
@ -352,19 +370,107 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *DBStore) AddUserToShare(share Share, username string, role ShareRole) 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 {
|
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 {
|
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 {
|
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) {
|
func (store *DBStore) GetShares() (shares []Share, err error) {
|
||||||
|
@ -372,22 +478,12 @@ func (store *DBStore) GetShares() (shares []Share, err error) {
|
||||||
var processingError error
|
var processingError error
|
||||||
|
|
||||||
if err := tx.AscendKeys(sharePrefix+"*", func(key, value string) bool {
|
if err := tx.AscendKeys(sharePrefix+"*", func(key, value string) bool {
|
||||||
var share Share
|
idString := strings.TrimPrefix(key, sharePrefix)
|
||||||
|
share, err := unmarshalShare(idString, value)
|
||||||
if err := json.Unmarshal([]byte(value), &share); err != nil {
|
if err != nil {
|
||||||
processingError = err
|
processingError = err
|
||||||
return false
|
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)
|
shares = append(shares, share)
|
||||||
return true
|
return true
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -400,10 +496,76 @@ func (store *DBStore) GetShares() (shares []Share, err error) {
|
||||||
return shares, err
|
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")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *DBStore) FindByUser(username string) ([]UserShare, error) {
|
func unmarshalShare(idString, payload string) (Share, error) {
|
||||||
panic("implement me")
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,6 +196,11 @@ func TestStoreShareHandling(t *testing.T) {
|
||||||
_ = store.AddUser(user1)
|
_ = store.AddUser(user1)
|
||||||
_ = store.AddUser(user2)
|
_ = store.AddUser(user2)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = store.RemoveUser(user1.Username)
|
||||||
|
_ = store.RemoveUser(user2.Username)
|
||||||
|
}()
|
||||||
|
|
||||||
t.Run("multiple shares should exist", func(t *testing.T) {
|
t.Run("multiple shares should exist", func(t *testing.T) {
|
||||||
shares, _ := store.GetShares()
|
shares, _ := store.GetShares()
|
||||||
if len(shares) != 3 {
|
if len(shares) != 3 {
|
||||||
|
@ -238,23 +243,25 @@ func TestStoreShareHandling(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("duplicate login not allowed", func(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.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("share is found by login", func(t *testing.T) {
|
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 {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if share.UUID != share1.UUID {
|
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) {
|
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)
|
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 {
|
if err := store.RemoveLogin(share1, user1.Username, login1.LoginName); err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
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.Errorf("share should not be found now, but returned: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("user can be removed", func(t *testing.T) {
|
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)
|
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.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) {
|
t.Run("database should be empty now", func(t *testing.T) {
|
||||||
// checks that we properly deleted all keys
|
// checks that we properly deleted all keys
|
||||||
if err := store.db.View(func(tx *buntdb.Tx) error {
|
if err := store.db.View(func(tx *buntdb.Tx) error {
|
||||||
return tx.Ascend("", func(key, value string) bool {
|
return tx.Ascend("", func(key, value string) bool {
|
||||||
t.Errorf("there should be no keys left")
|
t.Errorf("leftover key found: %v", key)
|
||||||
return false
|
return true
|
||||||
})
|
})
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Errorf("iterating keys failed: %v", err)
|
t.Errorf("iterating keys failed: %v", err)
|
||||||
|
|
Loading…
Reference in New Issue