lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 5b6f975d160bfd8e4c50e7d4dd7d9f4900a2cce6
parent eb753d2a89ba8b323904500b25c0c6a332772dcb
Author: Sean Enck <sean@ttypty.com>
Date:   Tue,  8 Oct 2024 18:15:40 -0400

switch off go-envparse, cleanup config/shell input handling a bit

Diffstat:
Mcmd/main.go | 34++--------------------------------
Mgo.mod | 1-
Mgo.sum | 2--
Minternal/app/core_test.go | 2+-
Minternal/config/core.go | 47++++++++++++++++++++++++++++++++++-------------
Minternal/config/core_test.go | 27+++++++++++++++++++++++++++
Minternal/config/vars.go | 8++++++++
Minternal/config/vars_test.go | 2+-
Minternal/platform/os.go | 39+++++++++++++++++++++++++++++++++++++++
Minternal/platform/os_test.go | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 170 insertions(+), 50 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -2,7 +2,6 @@ package main import ( - "bytes" "errors" "fmt" "os" @@ -10,8 +9,6 @@ import ( "strings" "time" - env "github.com/hashicorp/go-envparse" - "github.com/seanenck/lockbox/internal/app" "github.com/seanenck/lockbox/internal/config" "github.com/seanenck/lockbox/internal/platform" @@ -48,35 +45,8 @@ func run() error { if err != nil { return err } - for _, useEnv := range paths { - if !platform.PathExists(useEnv) { - continue - } - b, err := os.ReadFile(useEnv) - if err != nil { - return err - } - r := bytes.NewReader(b) - found, err := env.Parse(r) - if err != nil { - return err - } - result, err := config.ExpandParsed(found) - if err != nil { - return err - } - for k, v := range result { - ok, err := config.IsUnset(k, v) - if err != nil { - return err - } - if !ok { - if err := os.Setenv(k, v); err != nil { - return err - } - } - } - break + if err := platform.LoadEnvConfigs(paths...); err != nil { + return err } args := os.Args if len(args) < 2 { diff --git a/go.mod b/go.mod @@ -4,7 +4,6 @@ go 1.23.0 require ( github.com/aymanbagabas/go-osc52 v1.2.2 - github.com/hashicorp/go-envparse v0.1.0 github.com/pquerna/otp v1.4.0 github.com/tobischo/gokeepasslib/v3 v3.6.0 golang.org/x/text v0.19.0 diff --git a/go.sum b/go.sum @@ -10,8 +10,6 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= -github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/internal/app/core_test.go b/internal/app/core_test.go @@ -13,7 +13,7 @@ func TestUsage(t *testing.T) { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = app.Usage(true, "lb") - if len(u) != 115 { + if len(u) != 116 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/config/core.go b/internal/config/core.go @@ -135,7 +135,11 @@ func (e environmentBase) Key() string { // Get will get the boolean value for the setting func (e EnvironmentBool) Get() (bool, error) { - read := strings.ToLower(strings.TrimSpace(getExpand(e.Key()))) + return parseStringYesNo(e, getExpand(e.Key())) +} + +func parseStringYesNo(e EnvironmentBool, in string) (bool, error) { + read := strings.ToLower(strings.TrimSpace(in)) switch read { case no: return false, nil @@ -361,6 +365,18 @@ func Environ() []string { return results } +func parseConfigKeyEarly[T any](env interface { + Key() string + Get() (T, error) +}, inputs map[string]string, conv func(string) (T, error), +) (T, error) { + raw, ok := inputs[env.Key()] + if ok { + return conv(raw) + } + return env.Get() +} + // ExpandParsed handles cycles of parsing configuration env inputs to resolve ALL variables func ExpandParsed(inputs map[string]string) (map[string]string, error) { if inputs == nil { @@ -369,14 +385,13 @@ func ExpandParsed(inputs map[string]string) (map[string]string, error) { if len(inputs) == 0 { return inputs, nil } - var err error - var cycles int - possibleCycles, ok := inputs[envConfigExpands.Key()] - if ok { - cycles, err = strconv.Atoi(possibleCycles) - } else { - cycles, err = envConfigExpands.Get() + cycles, err := parseConfigKeyEarly(envConfigExpands, inputs, strconv.Atoi) + if err != nil { + return nil, err } + quoted, err := parseConfigKeyEarly(envConfigQuoted, inputs, func(v string) (bool, error) { + return parseStringYesNo(envConfigQuoted, v) + }) if err != nil { return nil, err } @@ -385,7 +400,7 @@ func ExpandParsed(inputs map[string]string) (map[string]string, error) { } result := inputs for cycles > 0 { - expanded := expandParsed(result) + expanded := expandParsed(result, quoted) if len(expanded) == len(result) { same := true for k, v := range expanded { @@ -409,12 +424,18 @@ func ExpandParsed(inputs map[string]string) (map[string]string, error) { return nil, errors.New("reached maximum expand cycle count") } -func expandParsed(inputs map[string]string) map[string]string { +func expandParsed(inputs map[string]string, quoted bool) map[string]string { result := make(map[string]string) for k, v := range inputs { - result[k] = os.Expand(v, func(in string) string { - if val, ok := inputs[in]; ok { - return val + val := v + if quoted { + if strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"") { + val = strings.TrimPrefix(strings.TrimSuffix(val, "\""), "\"") + } + } + result[k] = os.Expand(val, func(in string) string { + if i, ok := inputs[in]; ok { + return i } return os.Getenv(in) }) diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -231,6 +231,33 @@ func TestExpandParsed(t *testing.T) { if err != nil || len(r) != 2 || r["TEST"] != "$TEST_ABC" { t.Errorf("invalid expand: %v", r) } + ins["LOCKBOX_ENV_EXPANDS"] = "5" + ins["TEST"] = "\"abc\"" + os.Setenv("LOCKBOX_ENV_QUOTED", "yes") + r, err = config.ExpandParsed(ins) + if err != nil || len(r) != 2 || r["TEST"] != "abc" { + t.Errorf("invalid expand: %v", r) + } + os.Setenv("LOCKBOX_ENV_QUOTED", "no") + ins["TEST"] = "\"abc\"" + r, err = config.ExpandParsed(ins) + if err != nil || len(r) != 2 || r["TEST"] != "\"abc\"" { + t.Errorf("invalid expand: %v", r) + } + os.Unsetenv("LOCKBOX_ENV_QUOTED") + r, err = config.ExpandParsed(ins) + if err != nil || len(r) != 2 || r["TEST"] != "abc" { + t.Errorf("invalid expand: %v", r) + } + ins["LOCKBOX_ENV_QUOTED"] = "yes" + r, err = config.ExpandParsed(ins) + if err != nil || len(r) != 3 || r["TEST"] != "abc" { + t.Errorf("invalid expand: %v", r) + } + ins["LOCKBOX_ENV_QUOTED"] = "1" + if _, err = config.ExpandParsed(ins); err == nil || err.Error() != "invalid yes/no env value for LOCKBOX_ENV_QUOTED" { + t.Errorf("invalid error: %v", err) + } } func TestWrap(t *testing.T) { diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -314,6 +314,14 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode), allowed: []string{commandArgsExample, "password"}, canDefault: false, }) + envConfigQuoted = environmentRegister( + EnvironmentBool{ + environmentDefault: newDefaultedEnvironment(true, + environmentBase{ + subKey: EnvConfig.subKey + "_QUOTED", + desc: "Enables removing prefix/suffix quotes from shell environment config settings\nwhen loaded through configuration file.", + }), + }) envConfigExpands = environmentRegister( EnvironmentInt{ environmentDefault: newDefaultedEnvironment(20, diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -105,7 +105,7 @@ func TestListVariables(t *testing.T) { known[trim] = struct{}{} } l := len(known) - if l != 33 { + if l != 34 { t.Errorf("invalid env count, outdated? %d", l) } } diff --git a/internal/platform/os.go b/internal/platform/os.go @@ -9,6 +9,9 @@ import ( "os" "strings" "syscall" + + "github.com/seanenck/lockbox/internal/config" + "mvdan.cc/sh/v3/expand" ) func termEcho(on bool) { @@ -138,3 +141,39 @@ func PathExists(file string) bool { } return true } + +// LoadEnvConfigs load environment settings from configs +func LoadEnvConfigs(paths ...string) error { + for _, useEnv := range paths { + if !PathExists(useEnv) { + continue + } + b, err := os.ReadFile(useEnv) + if err != nil { + return err + } + env := expand.ListEnviron(strings.Split(string(b), "\n")...) + found := make(map[string]string) + env.Each(func(name string, vr expand.Variable) bool { + found[name] = vr.String() + return true + }) + result, err := config.ExpandParsed(found) + if err != nil { + return err + } + for k, v := range result { + ok, err := config.IsUnset(k, v) + if err != nil { + return err + } + if !ok { + if err := os.Setenv(k, v); err != nil { + return err + } + } + } + break + } + return nil +} diff --git a/internal/platform/os_test.go b/internal/platform/os_test.go @@ -1,8 +1,10 @@ package platform_test import ( + "fmt" "os" "path/filepath" + "strings" "testing" "github.com/seanenck/lockbox/internal/platform" @@ -19,3 +21,59 @@ func TestPathExist(t *testing.T) { t.Error("test dir SHOULD exist") } } + +func TestLoadEnvConfigs(t *testing.T) { + os.Clearenv() + defer os.Clearenv() + files := []string{filepath.Join("testdata", "xyz"), filepath.Join("testdata", "abc")} + if err := platform.LoadEnvConfigs(files...); err != nil { + t.Errorf("unexpected error: %v", err) + } + cfg := files[1] + os.WriteFile(cfg, []byte(` +TEST_X=1 +TEST_Y=2 +TEST_Z="1 +TEST11=1" +TEST_3="abc $HOME $X $TEST_X"`), 0o644) + if err := platform.LoadEnvConfigs(files...); err != nil { + t.Errorf("unexpected error: %v", err) + } + env := fmt.Sprintf("%v", os.Environ()) + verify := func(expects []string) { + for _, e := range expects { + if !strings.Contains(env, e) { + t.Errorf("invalid env: %s (missing '%s')", env, e) + } + } + } + verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc 1", "TEST_Z=\"1", "TEST11=1\""}) + os.Setenv("HOME", "a123") + if err := platform.LoadEnvConfigs(files...); err != nil { + t.Errorf("unexpected error: %v", err) + } + env = fmt.Sprintf("%v", os.Environ()) + verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc a123 1", "TEST_Z=\"1", "TEST11=1\""}) + os.Setenv("XYZ", "xyz") + os.Setenv("HOME", "$TEST4") + os.Setenv("TEST4", "$XYZ") + if err := platform.LoadEnvConfigs(files...); err != nil { + t.Errorf("unexpected error: %v", err) + } + env = fmt.Sprintf("%v", os.Environ()) + verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc xyz 1", "TEST_Z=\"1", "TEST11=1\""}) + final := filepath.Join("testdata", "zzz") + files = append(files, final) + os.Setenv("TEST4", "a") + os.WriteFile(final, []byte(` +TEST_X=1 +TEST_Y=2 +TEST_Z="1 +TEST11=2" +TEST_3="abc $HOME $X $TEST_X"`), 0o644) + if err := platform.LoadEnvConfigs(files...); err != nil { + t.Errorf("unexpected error: %v", err) + } + env = fmt.Sprintf("%v", os.Environ()) + verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc a 1", "TEST_Z=\"1", "TEST11=1\""}) +}