commit fde306fae016696a2298addffa1f07e4eeffb1fc
parent 203dcc2fcbca88f70d1b6bf725461fb7f78f1567
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 7 Jun 2025 17:07:05 -0400
add help about fields, better field error checking
Diffstat:
8 files changed, 60 insertions(+), 19 deletions(-)
diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log
@@ -1,7 +1,7 @@
password: test1/key1/password
-input paths must contain at LEAST 2 components
-unknown entity field:
-unknown entity field: still
+'test3' is not an allowed field name
+'' is not an allowed field name
+'still' is not an allowed field name
otp can NOT be multi-line
password can NOT be multi-line
testing5
diff --git a/internal/app/help/core.go b/internal/app/help/core.go
@@ -11,6 +11,7 @@ import (
"text/template"
"git.sr.ht/~enckse/lockbox/internal/app/commands"
+ "git.sr.ht/~enckse/lockbox/internal/backend"
"git.sr.ht/~enckse/lockbox/internal/config"
"git.sr.ht/~enckse/lockbox/internal/output"
)
@@ -43,6 +44,10 @@ type (
KeyFile string
NoKey string
}
+ Database struct {
+ Fields string
+ Examples string
+ }
}
)
@@ -112,6 +117,19 @@ func Usage(verbose bool, exe string) ([]string, error) {
document.Config.XDG = config.ConfigXDG
document.ReKey.KeyFile = setDocFlag(commands.ReKeyFlags.KeyFile)
document.ReKey.NoKey = commands.ReKeyFlags.NoKey
+ var fields []string
+ for _, field := range backend.AllowedFields {
+ fields = append(fields, strings.ToLower(field))
+ }
+ sort.Strings(fields)
+ document.Database.Fields = strings.Join(fields, ", ")
+ var examples []string
+ for _, example := range []string{commands.Insert, commands.Show} {
+ for _, field := range fields {
+ examples = append(examples, fmt.Sprintf("%s %s my/path/%s", document.Executable, example, field))
+ }
+ }
+ document.Database.Examples = strings.Join(examples, "\n\n")
files, err := docs.ReadDir(docDir)
if err != nil {
return nil, err
diff --git a/internal/app/help/doc/database.txt b/internal/app/help/doc/database.txt
@@ -6,3 +6,10 @@ is possible to use the database in those applications, just take caution when
changing it outside of '{{ $.Executable }}' usage. If a database not normally used by '{{ $.Executable }}' is
to be used by '{{ $.Executable }}', try using the various readonly settings to control
interactions.
+
+When using `{{ $.Executable }}` one can only insert/manage the following
+fields: {{ $.Database.Fields }}
+
+Example commands:
+
+{{ $.Database.Examples }}
diff --git a/internal/app/insert.go b/internal/app/insert.go
@@ -4,6 +4,7 @@ package app
import (
"errors"
"fmt"
+ "slices"
"strings"
"git.sr.ht/~enckse/lockbox/internal/backend"
@@ -18,6 +19,12 @@ func Insert(cmd UserInputOptions) error {
}
entry := args[0]
base := backend.Base(entry)
+ if !slices.ContainsFunc(backend.AllowedFields, func(v string) bool {
+ return base == strings.ToLower(v)
+ }) {
+ return fmt.Errorf("'%s' is not an allowed field name", base)
+ }
+
dir := backend.Directory(entry)
existing, err := t.Get(dir, backend.SecretValue)
if err != nil {
@@ -33,7 +40,7 @@ func Insert(cmd UserInputOptions) error {
}
}
}
- password, err := cmd.Input(!isPipe && !strings.EqualFold(base, backend.Notes))
+ password, err := cmd.Input(!isPipe && !strings.EqualFold(base, backend.NotesField))
if err != nil {
return fmt.Errorf("invalid input: %w", err)
}
diff --git a/internal/app/insert_test.go b/internal/app/insert_test.go
@@ -65,7 +65,7 @@ func TestInsertDo(t *testing.T) {
m.pipe = func() bool {
return false
}
- m.command.args = []string{"test/test2/test3"}
+ m.command.args = []string{"test/test2/test3ss/password"}
m.command.confirm = false
m.input = func() ([]byte, error) {
return nil, errors.New("failure")
@@ -75,12 +75,21 @@ func TestInsertDo(t *testing.T) {
t.Errorf("invalid error: %v", err)
}
m.command.confirm = false
+ m.command.args = []string{"test/test2/test3/password"}
m.pipe = func() bool {
return true
}
if err := app.Insert(m); err == nil || err.Error() != "invalid input: failure" {
t.Errorf("invalid error: %v", err)
}
+ m.command.confirm = false
+ m.command.args = []string{"test/test2/test3/Password"}
+ m.pipe = func() bool {
+ return true
+ }
+ if err := app.Insert(m); err == nil || err.Error() != "'Password' is not an allowed field name" {
+ t.Errorf("invalid error: %v", err)
+ }
m.input = func() ([]byte, error) {
return []byte("TEST"), nil
}
diff --git a/internal/app/totp.go b/internal/app/totp.go
@@ -97,7 +97,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error {
if !interactive && clipMode {
return errors.New("clipboard not available in non-interactive mode")
}
- if !backend.IsLeafAttribute(args.Entry, backend.OTP) {
+ if !backend.IsLeafAttribute(args.Entry, backend.OTPField) {
return fmt.Errorf("'%s' is not a TOTP entry", args.Entry)
}
entity, err := getEntity(args.Entry, opts.app)
@@ -219,7 +219,7 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error {
return ErrNoTOTP
}
if args.Mode == ListTOTPMode {
- return doList(backend.OTP, args.Entry, opts.app, false)
+ return doList(backend.OTPField, args.Entry, opts.app, false)
}
return args.display(opts)
}
diff --git a/internal/backend/actions.go b/internal/backend/actions.go
@@ -185,7 +185,7 @@ func (t *Transaction) Move(src *Entity, dst string) error {
values := make(map[string]string)
for k, v := range src.Values {
found := false
- for _, mapping := range allowedFields {
+ for _, mapping := range AllowedFields {
if strings.EqualFold(k, mapping) {
values[mapping] = v
found = true
@@ -225,11 +225,11 @@ func (t *Transaction) Move(src *Entity, dst string) error {
for k, v := range values {
val := v
switch k {
- case otpKey, passKey:
+ case OTPField, PasswordField:
if strings.Contains(val, "\n") {
return fmt.Errorf("%s can NOT be multi-line", strings.ToLower(k))
}
- if k == otpKey {
+ if k == OTPField {
val = config.EnvTOTPFormat.Get(v)
}
}
diff --git a/internal/backend/core.go b/internal/backend/core.go
@@ -15,22 +15,22 @@ import (
)
var (
- errPath = errors.New("input paths must contain at LEAST 2 components")
- allowedFields = []string{notesKey, passKey, otpKey}
+ errPath = errors.New("input paths must contain at LEAST 2 components")
+ // AllowedFields are the same of allowed names for storing in a kdbx entry
+ AllowedFields = []string{NotesField, OTPField, PasswordField}
)
const (
- notesKey = "Notes"
titleKey = "Title"
- passKey = "Password"
pathSep = "/"
isGlob = pathSep + "*"
modTimeKey = "ModTime"
- otpKey = "otp"
- // OTP is the totp storage attribute
- OTP = otpKey
- // Notes is the multiline notes key
- Notes = notesKey
+ // OTPField is the totp storage attribute
+ OTPField = "otp"
+ // NotesField is the multiline notes key
+ NotesField = "Notes"
+ // PasswordField is where the password is stored
+ PasswordField = "Password"
)
type (