lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 842971fe5f4ec89e574c1923e9149ed8cb3e4c03
parent e10bf75c17eb69901bc7f7ff439199a5a18dcca0
Author: Sean Enck <sean@ttypty.com>
Date:   Fri, 18 Jul 2025 21:56:35 -0400

add a basic 'health' command

Diffstat:
Mcmd/lb/main.go | 2++
Mcmd/lb/main_test.go | 3+++
Mcmd/lb/tests/expected.log | 9+++++++++
Minternal/app/commands/core.go | 4++--
Ainternal/app/health.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/health_test.go | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/app/help/core.go | 1+
Minternal/app/help/core_test.go | 4++--
8 files changed, 160 insertions(+), 4 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -78,6 +78,8 @@ func run() error { return fmt.Errorf("%s is not allowed in read-only", command) } switch command { + case commands.Health: + return app.Health(p) case commands.ReKey: return app.ReKey(p) case commands.List, commands.Groups: diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -344,6 +344,9 @@ func test(profile string) error { setConfig(r.config) r.run("", "ls") + r.section("health") + r.run("", "health") + r.section("env") r.run("", fmt.Sprintf("vars | sed 's#/%s#/datadir#g' | grep -v CREDENTIALS | sort", profile)) diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -321,6 +321,15 @@ test5/multiline/notes test6/multiline/notes test6/multiline/otp test6/multiline/password +health +key + -> ok +keyfile + -> ok +clipboard + -> ok +store + -> ok env LOCKBOX_CLIP_COPY=[touch testdata/datadir/clip.copy] LOCKBOX_JSON_HASH_LENGTH=3 diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go @@ -64,8 +64,8 @@ const ( TOTPURL = "url" // TOTPSeed will display the seed for the TOTP tokens TOTPSeed = "seed" - // ClipManagerStop will stop the clipboard manager - ClipManagerStop = "-kill" + // Health will show health information (for debugging/troubleshooting) + Health = "health" ) var ( diff --git a/internal/app/health.go b/internal/app/health.go @@ -0,0 +1,52 @@ +// Package app can insert +package app + +import ( + "errors" + "fmt" + "io" + + "git.sr.ht/~enckse/lockbox/internal/config" + "git.sr.ht/~enckse/lockbox/internal/platform" +) + +func report(w io.Writer, cat string, err error) { + msg := "ok" + if err != nil { + msg = fmt.Sprintf("error: %v", err) + } + text := fmt.Sprintf("%s\n -> %s\n", cat, msg) + w.Write([]byte(text)) +} + +// Health will display configuration/system health +func Health(cmd CommandOptions) error { + key, err := config.NewKey(config.DefaultKeyMode) + w := cmd.Writer() + if err == nil { + _, err = key.Read() + } + report(w, "key", err) + err = nil + file := config.EnvKeyFile.Get() + if file != "" { + err = errors.New("key file set, does not exist") + + if platform.PathExists(file) { + err = nil + } + } + report(w, "keyfile", err) + _, err = platform.NewClipboard(platform.DefaultClipboardLoader{}) + report(w, "clipboard", err) + store := config.EnvStore.Get() + err = errors.New("store not set") + if store != "" { + err = errors.New("store does not exist") + if platform.PathExists(store) { + err = nil + } + } + report(w, "store", err) + return nil +} diff --git a/internal/app/health_test.go b/internal/app/health_test.go @@ -0,0 +1,89 @@ +package app_test + +import ( + "strings" + "testing" + + "git.sr.ht/~enckse/lockbox/internal/app" + "git.sr.ht/~enckse/lockbox/internal/config/store" +) + +func TestHealth(t *testing.T) { + m := newMockCommand(t) + database, _ := store.GetString("LOCKBOX_STORE") + store.Clear() + store.SetBool("LOCKBOX_FEATURE_CLIP", false) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s := m.buf.String() + if strings.Count(s, "ok") != 1 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "clip feature is disabled") || !strings.Contains(s, "store not set") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetBool("LOCKBOX_FEATURE_CLIP", true) + store.SetArray("LOCKBOX_CLIP_COPY", []string{"x"}) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "store not set") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_STORE", "xxxxxx") + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "store does not exist") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_STORE", database) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key MUST be set in this key mode") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", "xxxxx") + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "key file set, does not exist") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", database) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key MUST be set in this key mode") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"zoiajfoaijfoeaijo1j091"}) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key command failed") { + t.Errorf("invalid health: %s", s) + } + m.buf.Reset() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"pass"}) + if err := app.Health(m); err != nil { + t.Errorf("invalid error: %v", err) + } + s = m.buf.String() + if strings.Count(s, "ok") != 4 { + t.Errorf("invalid health: %s", s) + } +} diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -92,6 +92,7 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, command(commands.Move, fmt.Sprintf("%s %s", isGroup, isGroup), "move a group from source to destination")) results = append(results, command(commands.ReKey, "", "rekey/reinitialize the database credentials")) results = append(results, command(commands.Remove, isGroup, "remove an entry from the store")) + results = append(results, command(commands.Health, "", "display configuration health")) results = append(results, command(commands.JSON, isFilter, "display detailed information")) results = append(results, command(commands.List, isFilter, "list entries")) results = append(results, command(commands.Groups, isFilter, "list groups")) diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -9,11 +9,11 @@ import ( func TestUsage(t *testing.T) { u, _ := help.Usage(false, "lb") - if len(u) != 27 { + if len(u) != 28 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 128 { + if len(u) != 129 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u {