lockbox

password manager
Log | Files | Refs | README | LICENSE

commit de4e62f15af731ebe603b3924d567d71a88c5c2a
parent c1aae52bb7e1e929575bf049e0f9e5befd0cb69f
Author: Sean Enck <sean@ttypty.com>
Date:   Sun,  2 Oct 2022 10:54:57 -0400

adding mv

Diffstat:
Mcmd/main.go | 24+++++++++++++++++++++++-
Mcmd/vers.txt | 2+-
Mcontrib/completions.bash | 8+++++++-
Minternal/backend/actions.go | 32+++++++++++++++++++++++---------
Minternal/backend/actions_test.go | 48+++++++++++++++++++++++++++++++++++++-----------
Minternal/backend/query_test.go | 10+++++-----
Mtests/expected.log | 7+++++--
Mtests/run.sh | 3+++
8 files changed, 104 insertions(+), 30 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -98,6 +98,28 @@ func run() *programError { } case "version": fmt.Printf("version: %s\n", strings.TrimSpace(version)) + case "mv": + if len(args) != 4 { + return newError("mv requires src and dst", errors.New("src/dst required")) + } + src := getEntry(args, 2) + dst := getEntry(args, 3) + srcExists, err := t.Get(src, backend.SecretValue) + if err != nil { + return newError("unable to get source object", errors.New("failed to get source")) + } + if srcExists == nil { + return newError("no source object found", errors.New("source object required")) + } + dstExists, err := t.Get(dst, backend.BlankValue) + if err != nil { + return newError("unable to get destination object", errors.New("failed to get destination")) + } + if dstExists != nil { + if !confirm("overwrite destination") { + return nil + } + } case "insert": options := cli.Arguments{} idx := 2 @@ -132,7 +154,7 @@ func run() *programError { return newError("invalid input", err) } p := strings.TrimSpace(string(password)) - if err := t.Insert(entry, p, len(strings.Split(p, "\n")) > 1); err != nil { + if err := t.Insert(entry, p); err != nil { return newError("failed to insert", err) } fmt.Println("") diff --git a/cmd/vers.txt b/cmd/vers.txt @@ -1 +1 @@ -v22.10.02 +v22.10.03 diff --git a/contrib/completions.bash b/contrib/completions.bash @@ -18,7 +18,7 @@ _lb() { fi cur=${COMP_WORDS[COMP_CWORD]} if [ "$COMP_CWORD" -eq 1 ]; then - opts="version ls show insert rm totp find$clip_enabled" + opts="version ls show insert rm totp mv find$clip_enabled" # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) else @@ -27,6 +27,9 @@ _lb() { "insert") opts="-multi $(lb ls)" ;; + "mv") + opts=$(lb ls) + ;; "totp") opts="-once -short "$(lb totp -list) if [ -n "$clip_enabled" ]; then @@ -52,6 +55,9 @@ _lb() { opts="-multi" fi ;; + "mv") + opts=$(lb ls) + ;; "totp") needs=0 if [ "${COMP_WORDS[2]}" == "-once" ] || [ "${COMP_WORDS[2]}" == "-short" ]; then diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -156,33 +156,47 @@ func splitComponents(path string) ([]string, string, error) { return parts, title, nil } -// Insert handles inserting a new element -func (t *Transaction) Insert(path, val string, multi bool) error { - if strings.TrimSpace(path) == "" { +// Move will move a src object to a dst location +func (t *Transaction) Move(src QueryEntity, dst string) error { + if strings.TrimSpace(src.Path) == "" { return errors.New("empty path not allowed") } - if strings.TrimSpace(val) == "" { + if strings.TrimSpace(src.Value) == "" { return errors.New("empty secret not allowed") } - offset, title, err := splitComponents(path) + dOffset, dTitle, err := splitComponents(dst) if err != nil { return err } + sOffset, sTitle, err := splitComponents(src.Path) + if err != nil { + return err + } + isMove := dst != src.Path + multi := len(strings.Split(strings.TrimSpace(src.Value), "\n")) > 1 return t.change(func(c Context) error { - c.removeEntity(offset, title) + c.removeEntity(sOffset, sTitle) + if isMove { + c.removeEntity(dOffset, dTitle) + } e := gokeepasslib.NewEntry() - e.Values = append(e.Values, value(titleKey, title)) + e.Values = append(e.Values, value(titleKey, dTitle)) field := passKey if multi { field = notesKey } - e.Values = append(e.Values, protectedValue(field, val)) - c.insertEntity(offset, title, e) + e.Values = append(e.Values, protectedValue(field, src.Value)) + c.insertEntity(dOffset, dTitle, e) return nil }) } +// Insert is a move to the same location +func (t *Transaction) Insert(path, val string) error { + return t.Move(QueryEntity{Path: path, Value: val}, path) +} + // Remove handles remove an element func (t *Transaction) Remove(entity *QueryEntity) error { if entity == nil { diff --git a/internal/backend/actions_test.go b/internal/backend/actions_test.go @@ -29,31 +29,57 @@ func setup(t *testing.T) *backend.Transaction { func TestBadAction(t *testing.T) { tr := &backend.Transaction{} - if err := tr.Insert("a/a/a", "a", false); err.Error() != "invalid transaction" { + if err := tr.Insert("a/a/a", "a"); err.Error() != "invalid transaction" { t.Errorf("wrong error: %v", err) } } +func TestMove(t *testing.T) { + setup(t) + fullSetup(t, true).Insert(filepath.Join("test", "test2", "test1"), "pass") + fullSetup(t, true).Insert(filepath.Join("test", "test2", "test3"), "pass") + if err := fullSetup(t, true).Move(backend.QueryEntity{Path: filepath.Join("test", "test2", "test3"), Value: "abc"}, filepath.Join("test1", "test2", "test3")); err != nil { + t.Errorf("no error: %v", err) + } + q, err := fullSetup(t, true).Get(filepath.Join("test1", "test2", "test3"), backend.SecretValue) + if err != nil { + t.Errorf("no error: %v", err) + } + if q.Value != "abc" { + t.Errorf("invalid retrieval") + } + if err := fullSetup(t, true).Move(backend.QueryEntity{Path: filepath.Join("test", "test2", "test1"), Value: "test"}, filepath.Join("test1", "test2", "test3")); err != nil { + t.Errorf("no error: %v", err) + } + q, err = fullSetup(t, true).Get(filepath.Join("test1", "test2", "test3"), backend.SecretValue) + if err != nil { + t.Errorf("no error: %v", err) + } + if q.Value != "test" { + t.Errorf("invalid retrieval") + } +} + func TestInserts(t *testing.T) { - if err := setup(t).Insert("", "", false); err.Error() != "empty path not allowed" { + if err := setup(t).Insert("", ""); err.Error() != "empty path not allowed" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Insert(filepath.Join("test", "offset"), "test", false); err.Error() != "invalid component path" { + if err := setup(t).Insert(filepath.Join("test", "offset"), "test"); err.Error() != "invalid component path" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Insert("test", "test", false); err.Error() != "invalid component path" { + if err := setup(t).Insert("test", "test"); err.Error() != "invalid component path" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Insert("a", "", false); err.Error() != "empty secret not allowed" { + if err := setup(t).Insert("a", ""); err.Error() != "empty secret not allowed" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Insert(filepath.Join("test", "offset", "value"), "pass", false); err != nil { + if err := setup(t).Insert(filepath.Join("test", "offset", "value"), "pass"); err != nil { t.Errorf("no error: %v", err) } - if err := fullSetup(t, true).Insert(filepath.Join("test", "offset", "value"), "pass2", false); err != nil { + if err := fullSetup(t, true).Insert(filepath.Join("test", "offset", "value"), "pass2"); err != nil { t.Errorf("wrong error: %v", err) } - if err := fullSetup(t, true).Insert(filepath.Join("test", "offset", "value2"), "pass", true); err != nil { + if err := fullSetup(t, true).Insert(filepath.Join("test", "offset", "value2"), "pass\npass"); err != nil { t.Errorf("no error: %v", err) } q, err := fullSetup(t, true).Get(filepath.Join("test", "offset", "value"), backend.SecretValue) @@ -67,7 +93,7 @@ func TestInserts(t *testing.T) { if err != nil { t.Errorf("no error: %v", err) } - if q.Value != "pass" { + if q.Value != "pass\npass" { t.Errorf("invalid retrieval") } } @@ -84,7 +110,7 @@ func TestRemoves(t *testing.T) { } setup(t) for _, i := range []string{"test1", "test2"} { - fullSetup(t, true).Insert(filepath.Join(i, i, i), "pass", false) + fullSetup(t, true).Insert(filepath.Join(i, i, i), "pass") } if err := fullSetup(t, true).Remove(&backend.QueryEntity{Path: filepath.Join("test1", "test1", "test1")}); err != nil { t.Errorf("wrong error: %v", err) @@ -97,7 +123,7 @@ func TestRemoves(t *testing.T) { } setup(t) for _, i := range []string{filepath.Join("test", "test", "test1"), filepath.Join("test", "test", "test2"), filepath.Join("test", "test", "test3"), filepath.Join("test", "test1", "test2"), filepath.Join("test", "test1", "test5")} { - fullSetup(t, true).Insert(i, "pass", false) + fullSetup(t, true).Insert(i, "pass") } if err := fullSetup(t, true).Remove(&backend.QueryEntity{Path: "test/test/test3"}); err != nil { t.Errorf("wrong error: %v", err) diff --git a/internal/backend/query_test.go b/internal/backend/query_test.go @@ -9,10 +9,10 @@ import ( func setupInserts(t *testing.T) { setup(t) - fullSetup(t, true).Insert("test/test/abc", "tedst", false) - fullSetup(t, true).Insert("test/test/abcx", "tedst", false) - fullSetup(t, true).Insert("test/test/ab11c", "tdest", true) - fullSetup(t, true).Insert("test/test/abc1ak", "atest", false) + fullSetup(t, true).Insert("test/test/abc", "tedst") + fullSetup(t, true).Insert("test/test/abcx", "tedst") + fullSetup(t, true).Insert("test/test/ab11c", "tdest\ntest") + fullSetup(t, true).Insert("test/test/abc1ak", "atest") } func TestGet(t *testing.T) { @@ -50,7 +50,7 @@ func TestValueModes(t *testing.T) { if err != nil { t.Errorf("no error: %v", err) } - if q.Value != "tdest" { + if q.Value != "tdest\ntest" { t.Errorf("invalid result value: %s", q.Value) } } diff --git a/tests/expected.log b/tests/expected.log @@ -29,5 +29,8 @@ hash:132ab0244293c495a027cec12d0050598616daca888449920fc652719be0987830827d069ef test/k/totp: hash:7ef183065ba70aaa417b87ea0a96b7e550a938a52440c640a07537f7794d8a89e50078eca6a7cbcfacabd97a2db06d11e82ddf7556ca909c4df9fc0d006013b1 delete entry? (y/N) -delete entry? (y/N) delete entry? (y/N) unable to remove entry (entity is empty/invalid) -delete entry? (y/N) keys/k/one2 +delete entry? (y/N) +delete entry? (y/N) unable to remove entry (entity is empty/invalid) + +delete entry? (y/N) +keys/k/one2 diff --git a/tests/run.sh b/tests/run.sh @@ -31,8 +31,11 @@ _run() { yes 2>/dev/null | "$BIN/lb" rm keys2/k/three echo yes 2>/dev/null | "$BIN/lb" rm test/k/totp + echo yes 2>/dev/null | "$BIN/lb" rm test/k/one + echo yes 2>/dev/null | "$BIN/lb" rm key/a/one + echo "$BIN/lb" ls }