From b48b7f58e1d68c4cdcb8d898492cca484ee6744f Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Sun, 18 Oct 2020 10:33:29 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20initial=20share=20commandline?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd_share.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 5 +- store.go | 91 +++++++++++++++++++++---------------- store_test.go | 2 +- 4 files changed, 179 insertions(+), 42 deletions(-) create mode 100644 cmd_share.go diff --git a/cmd_share.go b/cmd_share.go new file mode 100644 index 0000000..0cf6325 --- /dev/null +++ b/cmd_share.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "os" + "path" + + uuid "github.com/satori/go.uuid" +) + +type CmdShare struct { + CmdList CmdShareList `cmd:"" name:"list" help:"List all shares."` + CmdCreate CmdShareCreate `cmd:"" name:"create" help:"Create a new share."` + CmdDelete CmdShareDelete `cmd:"" name:"delete" help:"Delete a share."` + CmdAddUser CmdShareAddUser `cmd:"" name:"add-user" help:"Add user to share."` + CmdAddLogin CmdShareAddLogin `cmd:"" name:"add-login" help:"Add login to share."` +} + +type CmdShareList struct{} + +func (cmd *CmdShareList) Run(app *app) error { + shares, err := app.shareStore.GetShares() + if err != nil { + return err + } + + for _, share := range shares { + fmt.Printf("* %s (%s)\n", share.UUID.String(), share.Name) + } + + return nil +} + +type CmdShareCreate struct { + Name string `name:"name" help:"Name of the share." required:""` +} + +func (cmd *CmdShareCreate) Run(app *app) error { + share, err := app.shareStore.CreateShare() + if err != nil { + return err + } + share.Name = cmd.Name + if err := app.shareStore.UpdateShareAttributes(share); err != nil { + // Best effort cleanup. + _ = app.shareStore.RemoveShare(share.UUID) + + return fmt.Errorf("cannot set share attributes: %v", err) + } + + if err := os.MkdirAll(path.Join(app.DataDirectory, share.UUID.String()), 0750); err != nil { + // Best effort cleanup. + _ = app.shareStore.RemoveShare(share.UUID) + + return fmt.Errorf("cannot create data dir: %v", err) + } + + fmt.Printf("Share created: %s\n", share.UUID.String()) + return nil +} + +type ShareIdentifier struct { + UUID uuid.UUID `arg:"" name:"id" help:"ID of the share to be deleted." required:""` +} + +type CmdShareDelete struct { + ShareIdentifier +} + +func (cmd *CmdShareDelete) Run(app *app) error { + if err := os.RemoveAll(path.Join(app.DataDirectory, cmd.UUID.String())); err != nil { + return fmt.Errorf("cannot remove data directory: %v", err) + } + + if err := app.shareStore.RemoveShare(cmd.UUID); err != nil { + return fmt.Errorf("cannot remove share: %v", err) + } + + return nil +} + +type CmdShareAddUser struct { + ShareIdentifier + Username string `arg:"" name:"username" help:"Username of the user to add."` + Role ShareRole `name:"role" help:"Role of the user (reader/writer/admin)" default:"writer"` +} + +func (cmd *CmdShareAddUser) Run(app *app) error { + if _, err := app.userStore.GetUser(cmd.Username); err != nil { + return err + } + + share := Share{UUID: cmd.UUID} + return app.shareStore.AddUserToShare(share, cmd.Username, cmd.Role) +} + +type CmdShareAddLogin struct { + ShareIdentifier + Username string `arg:"" name:"username" help:"Username of the user to add the login for."` + LoginName string `arg:"" name:"loginname" help:"Name f the login. Must be unique."` + ReadOnly bool `name:"readonly" help:"If set, the login can only read."` + PasswordParam +} + +func (cmd *CmdShareAddLogin) Run(app *app) error { + if _, err := app.userStore.GetUser(cmd.Username); err != nil { + return err + } + + password, err := cmd.acquirePassword() + if err != nil { + return fmt.Errorf("cannot acquire password for login: %w", err) + } + + share := Share{UUID: cmd.UUID} + login := Login{ + LoginName: cmd.LoginName, + Password: password, + ReadOnly: cmd.ReadOnly, + } + + return app.shareStore.AddLogin(share, cmd.Username, login) +} diff --git a/main.go b/main.go index f81471d..948e21b 100644 --- a/main.go +++ b/main.go @@ -36,9 +36,10 @@ import ( type app struct { config - CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."` + CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."` + CmdShare CmdShare `cmd:"" name:"share" help:"Manage shares."` - userStore UserStore + userStore UserStore shareStore ShareStore } diff --git a/store.go b/store.go index a5edfb0..f3ac563 100644 --- a/store.go +++ b/store.go @@ -77,9 +77,9 @@ const ( ) type User struct { - Username string - Password string - Role GlobalRole + Username string + Password string + Role GlobalRole } type Share struct { @@ -96,6 +96,7 @@ type ShareUser struct { type Login struct { LoginName string Password string + ReadOnly bool } // View on a share from the perspective of a user of that share. @@ -114,6 +115,8 @@ type LoginShare struct { const userPrefix = "user:" const sharePrefix = "share:" const loginSharePrefix = "loginshare:" +const shareuserPrefix = "shareuser:" +const shareloginPrefix = "sharelogin:" type DBStore struct { db *buntdb.DB @@ -127,6 +130,14 @@ func (s Share) key() string { return sharePrefix + s.UUID.String() } +func (s Share) userKey(username string) string { + return fmt.Sprintf("%s%s:%s", shareuserPrefix, s.UUID.String(), username) +} + +func (s Share) loginKey(username, loginName string) string { + return fmt.Sprintf("%s%s:%s:%s", shareloginPrefix, s.UUID.String(), username, loginName) +} + func NewDBStore(filename string) (*DBStore, error) { db, err := buntdb.Open(filename) if err != nil { @@ -333,7 +344,7 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error { share := Share{UUID: id} var userNames []string - usersPrefix := share.key() + ":user:" + usersPrefix := share.userKey("") if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool { userNames = append(userNames, strings.TrimPrefix(key, usersPrefix)) return true @@ -343,7 +354,7 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error { 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) + return fmt.Errorf("cannot remove user %q from share: %w", username, err) } } @@ -363,8 +374,8 @@ func (store *DBStore) AddUserToShare(share Share, username string, role ShareRol 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) + if _, _, err := tx.Set(share.userKey(username), string(role), nil); err != nil { + return fmt.Errorf("cannot set user: %w", err) } return nil }) @@ -372,7 +383,7 @@ func (store *DBStore) AddUserToShare(share Share, username string, role ShareRol func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error { var logins []string - loginsPrefix := share.key() + ":login:" + username + ":" + loginsPrefix := share.loginKey(username, "") if err := tx.AscendKeys(loginsPrefix+"*", func(key, value string) bool { logins = append(logins, strings.TrimPrefix(key, loginsPrefix)) return true @@ -383,18 +394,18 @@ func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username s for _, loginName := range logins { shareIdString, err := tx.Delete(loginSharePrefix + username + ":" + loginName) if err != nil { - return fmt.Errorf("cannot remove login ref: %v", err) + return fmt.Errorf("cannot remove login ref: %w", 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.loginKey(username, loginName)); err != nil { + return fmt.Errorf("cannot remove login %q: %w", loginName, err) } } - if _, err := tx.Delete(share.key() + ":user:" + username); err != nil { - return fmt.Errorf("cannot remove user: %v", err) + if _, err := tx.Delete(share.userKey(username)); err != nil { + return fmt.Errorf("cannot remove user: %w", err) } return nil @@ -415,7 +426,7 @@ func (store *DBStore) AddLogin(share Share, username string, login Login) error if _, err := tx.Get(share.key()); err != nil { return ErrShareNotFound } - if _, err := tx.Get(share.key() + ":user:" + username); err != nil { + if _, err := tx.Get(share.userKey(username)); err != nil { return ErrUserNotFound } @@ -426,14 +437,18 @@ func (store *DBStore) AddLogin(share Share, username string, login Login) error // 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) + return fmt.Errorf("cannot set login reference: %w", 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) + b, err := json.Marshal(login) + if err != nil { + return fmt.Errorf("cannot marshal login: %w", err) + } + if _, _, err := tx.Set(share.loginKey(username, login.LoginName), string(b), nil); err != nil { + return fmt.Errorf("cannot set login: %w", err) } return nil }) @@ -451,7 +466,7 @@ func (store *DBStore) RemoveLogin(share Share, username string, loginName string return ErrLoginNotFound } - if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil { + if _, err := tx.Delete(share.loginKey(username, loginName)); err != nil { return ErrLoginNotFound } @@ -499,37 +514,35 @@ func (store *DBStore) FindShareByLogin(username, loginName string) (loginShare L 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 } + userRole, err := tx.Get(share.userKey(username)) + if err == buntdb.ErrNotFound { + // TODO warn about inconsistency + return ErrShareNotFound + } else if err != nil { + return err + } + + loginPayload, err := tx.Get(share.loginKey(username, loginName)) + if err == buntdb.ErrNotFound { + // TODO warn about inconsistency + return ErrShareNotFound + } else if err != nil { + return err + } + loginShare.Share = share loginShare.ShareUser = ShareUser{ Username: username, Role: ShareRole(userRole), } - loginShare.Login = Login{ - LoginName: loginName, - Password: loginPassword, + if err := json.Unmarshal([]byte(loginPayload), &loginShare.Login); err != nil { + return fmt.Errorf("cannot unmarshal login: %w", err) } - return nil }) @@ -543,7 +556,7 @@ func (store *DBStore) FindSharesByUser(username string) ([]UserShare, error) { func unmarshalShare(idString, payload string) (Share, error) { var share Share if err := json.Unmarshal([]byte(payload), &share); err != nil { - return Share{}, err + return Share{}, fmt.Errorf("cannot unmarshal share: %w", err) } // Just in case ... diff --git a/store_test.go b/store_test.go index 7aae93e..f90f1ff 100644 --- a/store_test.go +++ b/store_test.go @@ -220,7 +220,7 @@ func TestStoreShareHandling(t *testing.T) { } t.Run("cannot add login if user doesn't exist", func(t *testing.T) { - if err := store.AddLogin(share3, user1.Username, Login{"foo", ""}); err == nil { + if err := store.AddLogin(share3, user1.Username, Login{"foo", "", false}); err == nil { t.Errorf("an error should have been returned") } else if err != ErrUserNotFound { t.Errorf("wrong error has been returned: %v", err)