lockbox

password manager
Log | Files | Refs | README | LICENSE

commit d8362a9f27a988eae9e245f0d295312360e0a14d
parent 10c0f563b4ffacbfd6d19adb2da76d2799762107
Author: Sean Enck <sean@ttypty.com>
Date:   Fri,  6 Dec 2024 21:36:55 -0500

toml becomes first class configuration means and documentation

Diffstat:
Minternal/app/core.go | 17++++++-----------
Minternal/app/core_test.go | 2+-
Dinternal/app/doc/environment.txt | 1-
Minternal/app/doc/toml.txt | 13++++++++-----
Minternal/config/core.go | 6+++---
Minternal/config/toml.go | 55+++++++++++++++++++++++++++++++++++++++++++++----------
Minternal/config/toml_test.go | 5+++--
Minternal/config/vars.go | 34++++++----------------------------
Minternal/config/vars_test.go | 59-----------------------------------------------------------
9 files changed, 72 insertions(+), 120 deletions(-)

diff --git a/internal/app/core.go b/internal/app/core.go @@ -278,19 +278,10 @@ func Usage(verbose bool, exe string) ([]string, error) { return nil, err } switch header { - case "[environment]": + case "[toml]": env = s default: buf.WriteString(s) - if header == "[toml]" { - buf.WriteString("========================================\nconfig.toml\n---\n") - def, err := config.DefaultTOML() - if err != nil { - return nil, err - } - buf.WriteString(def) - buf.WriteString("========================================\n\n") - } } } if env == "" { @@ -299,7 +290,11 @@ func Usage(verbose bool, exe string) ([]string, error) { buf.WriteString(env) results = append(results, strings.Split(strings.TrimSpace(buf.String()), "\n")...) results = append(results, "") - results = append(results, config.ListEnvironmentVariables()...) + toml, err := config.DefaultTOML() + if err != nil { + return nil, err + } + results = append(results, toml) } return append(usage, results...), nil } 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) != 215 { + if len(u) != 98 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/doc/environment.txt b/internal/app/doc/environment.txt @@ -1 +0,0 @@ -The following environment variables can alter how '{{ $.Executable }}' works. diff --git a/internal/app/doc/toml.txt b/internal/app/doc/toml.txt @@ -1,5 +1,8 @@ -the environment settings can also be set via TOML configuration -files. The TOML file is read, the values transformed into the -corresponding environment keys, values parsed (if not strings), -expanded for shell variables, and then set into the process. The -following is a outline of what is allowed in the TOML file: +The core components of `{{ $.Executable }}` are controlled via +environment settings, but these settings are secondary (overriden) +by TOML configuration file(s) if given. The following represents +all available environment and TOML settings as a TOML file with +comments indicating the mapping to the underlying environment +settings: + +--- diff --git a/internal/config/core.go b/internal/config/core.go @@ -44,7 +44,7 @@ var ( xdgPaths = []string{configDirOffsetFile, tomlFile} homePaths = []string{filepath.Join(configDir, configDirOffsetFile), filepath.Join(configDir, tomlFile)} exampleColorWindows = []string{strings.Join([]string{exampleColorWindow, exampleColorWindow, exampleColorWindow + "..."}, colorWindowDelimiter)} - registeredEnv = []printer{} + registry = map[string]printer{} ) type ( @@ -356,7 +356,7 @@ func IsUnset(k, v string) (bool, error) { func Environ() []string { var results []string for _, k := range os.Environ() { - for _, r := range registeredEnv { + for _, r := range registry { key := r.self().Key() if key == EnvConfig.Key() { continue @@ -427,7 +427,7 @@ func wrap(in string, maxLength int) string { } func environmentRegister[T printer](obj T) T { - registeredEnv = append(registeredEnv, obj) + registry[obj.self().Key()] = obj return obj } diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -121,9 +121,13 @@ func DefaultTOML() (string, error) { break } } - sub = fmt.Sprintf(`# environment map: %s + text, err := generateDetailText(item.Key) + if err != nil { + return "", err + } + sub = fmt.Sprintf(`%s %s = %s -`, item.Key, sub, field) +`, text, sub, field) had, ok := unmapped[key] if !ok { had = []string{} @@ -134,11 +138,21 @@ func DefaultTOML() (string, error) { } sort.Strings(keys) builder := strings.Builder{} - if _, err := fmt.Fprintf(&builder, `# include additional configs, can NOT nest, but does allow globs ('*') -%s = [] -`, isInclude); err != nil { + configEnv, err := generateDetailText(EnvConfig.Key()) + if err != nil { return "", err } + for _, header := range []string{configEnv, "\n", fmt.Sprintf(` +# include additional configs, can NOT nest, but does allow globs ('*') +# this field is not configurable via environment variables +# and it is not considered part of the environment either +# it is ONLY used during TOML configuration loading +%s = [] +`, isInclude), "\n"} { + if _, err := builder.WriteString(header); err != nil { + return "", err + } + } for _, k := range keys { if k != root { if _, err := fmt.Fprintf(&builder, "\n[%s]\n", k); err != nil { @@ -154,16 +168,37 @@ func DefaultTOML() (string, error) { return builder.String(), nil } +func generateDetailText(key string) (string, error) { + data, ok := registry[key] + if !ok { + return "", fmt.Errorf("unexpected configuration key has no environment settings: %s", key) + } + env := data.self() + value, allow := data.values() + if len(value) == 0 { + value = "(unset)" + } + description := Wrap(2, env.desc) + requirement := "optional/default" + r := strings.TrimSpace(env.requirement) + if r != "" { + requirement = r + } + var text []string + for _, line := range []string{fmt.Sprintf("environment: %s", key), fmt.Sprintf("description:\n%s", description), fmt.Sprintf("default: %s", requirement), fmt.Sprintf("option: %s", strings.Join(allow, "|"))} { + for _, comment := range strings.Split(line, "\n") { + text = append(text, fmt.Sprintf("# %s", comment)) + } + } + return strings.Join(text, "\n"), nil +} + // LoadConfig will read the input reader and use the loader to source configuration files func LoadConfig(r io.Reader, loader Loader) ([]ShellEnv, error) { m := make(map[string]interface{}) if err := overlayConfig(r, true, &m, loader); err != nil { return nil, err } - var allowed []string - for _, k := range registeredEnv { - allowed = append(allowed, k.self().Key()) - } m = flatten(m, "") var res []ShellEnv for k, v := range m { @@ -173,7 +208,7 @@ func LoadConfig(r io.Reader, loader Loader) ([]ShellEnv, error) { } else { export = environmentPrefix + export } - if !slices.Contains(allowed, export) { + if _, ok := registry[export]; !ok { return nil, fmt.Errorf("unknown key: %s (%s)", k, export) } value, ok := v.(string) diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -2,6 +2,7 @@ package config_test import ( "errors" + "fmt" "io" "os" "path/filepath" @@ -267,6 +268,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) { t.Errorf("invalid error: %v", err) } os.WriteFile(file, []byte(loaded), 0o644) + fmt.Println(loaded) if err := config.LoadConfigFile(file); err != nil { t.Errorf("invalid error: %v", err) } @@ -276,8 +278,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) { count++ } } - // NOTE: this is one less than available because the default config itself is not configurable...via the config - if count != expectEnv-1 { + if count != 31 { t.Errorf("invalid environment after load: %d", count) } } diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -7,7 +7,6 @@ import ( "fmt" "net/url" "os" - "sort" "strings" "time" ) @@ -68,7 +67,7 @@ var ( environmentDefault: newDefaultedEnvironment(0, environmentBase{ subKey: EnvJSONDataOutput.subKey + "_HASH_LENGTH", - desc: fmt.Sprintf("Maximum hash string length the JSON output should contain when '%s' mode is set for JSON output.", JSONOutputs.Hash), + desc: fmt.Sprintf("Maximum string length of the JSON value when '%s' mode is set for JSON output.", JSONOutputs.Hash), }), shortDesc: "hash length", allowZero: true, @@ -155,7 +154,7 @@ var ( environmentBase{ subKey: "MAX", cat: totpCategory, - desc: "Time, in seconds, in which to show a TOTP token before automatically exiting.", + desc: "Time, in seconds, to show a TOTP token before automatically exiting.", }), shortDesc: "max totp time", allowZero: false, @@ -283,7 +282,8 @@ The keyword '%s' will disable this functionality and the keyword '%s' will search for a file in the following paths in XDG_CONFIG_HOME (%s) or from the user's HOME (%s). Matches the first file found. -Note that this setting is not output as part of the environment.`, noEnvironment, detectEnvironment, strings.Join(xdgPaths, ","), strings.Join(homePaths, ",")), +Note that this value is not output as part of the environment, nor +can it be set via TOML configuration.`, noEnvironment, detectEnvironment, strings.Join(xdgPaths, ","), strings.Join(homePaths, ",")), }), canDefault: true, allowed: []string{detectEnvironment, fileExample, noEnvironment}, @@ -343,7 +343,7 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode), environmentBase{ subKey: "TEMPLATE", cat: genCategory, - desc: fmt.Sprintf("The go text template to use to format the chosen words into a password (use '%s' to include a '$' to avoid shell expansion issues). Fields available are Text, Position.Start, and Position.End.", TemplateVariable), + desc: fmt.Sprintf("The go text template to use to format the chosen words into a password (use '%s' to include a '$' to avoid shell expansion issues). Available fields: Text, Position.Start, and Position.End.", TemplateVariable), }), allowed: []string{"<go template>"}, canDefault: true, @@ -381,7 +381,7 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode), environmentBase{ subKey: "CHARS", cat: genCategory, - desc: "The set of allowed characters in output words (empty means any characters are allowed).", + desc: "The set of allowed characters in output words (empty means any character is allowed).", }), allowed: []string{"<list of characters>"}, canDefault: true, @@ -404,28 +404,6 @@ func GetReKey(args []string) (ReKeyArgs, error) { return ReKeyArgs{KeyFile: file, NoKey: noPass}, nil } -// ListEnvironmentVariables will print information about env variables -func ListEnvironmentVariables() []string { - var results []string - for _, item := range registeredEnv { - env := item.self() - value, allow := item.values() - if len(value) == 0 { - value = "(unset)" - } - description := Wrap(2, env.desc) - requirement := "optional/default" - r := strings.TrimSpace(env.requirement) - if r != "" { - requirement = r - } - text := fmt.Sprintf("\n%s\n%s requirement: %s\n default: %s\n options: %s\n", env.Key(), description, requirement, value, strings.Join(allow, "|")) - results = append(results, text) - } - sort.Strings(results) - return results -} - func formatterTOTP(key, value string) string { const ( otpAuth = "otpauth" diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -3,14 +3,11 @@ package config_test import ( "fmt" "os" - "strings" "testing" "github.com/seanenck/lockbox/internal/config" ) -const expectEnv = 32 - func checkYesNo(key string, t *testing.T, obj config.EnvironmentBool, onEmpty bool) { t.Setenv(key, "yes") c, err := obj.Get() @@ -94,24 +91,6 @@ func TestTOTP(t *testing.T) { } } -func TestListVariables(t *testing.T) { - known := make(map[string]struct{}) - for _, v := range config.ListEnvironmentVariables() { - trim := strings.Split(strings.TrimSpace(v), " ")[0] - if !strings.HasPrefix(trim, "LOCKBOX_") { - t.Errorf("invalid env: %s", v) - } - if _, ok := known[trim]; ok { - t.Errorf("invalid re-used env: %s", trim) - } - known[trim] = struct{}{} - } - l := len(known) - if l != expectEnv { - t.Errorf("invalid env count, outdated? %d", l) - } -} - func TestReKey(t *testing.T) { if _, err := config.GetReKey([]string{"-nokey"}); err == nil || err.Error() != "a key or keyfile must be passed for rekey" { t.Errorf("failed: %v", err) @@ -231,44 +210,6 @@ func checkInt(e config.EnvironmentInt, key, text string, def int, allowZero bool } } -func TestEnvironDefinitions(t *testing.T) { - os.Clearenv() - vals := config.ListEnvironmentVariables() - expect := make(map[string]struct{}) - found := false - for _, val := range vals { - found = true - env := strings.Split(strings.TrimSpace(val), "\n")[0] - if !strings.HasPrefix(env, "LOCKBOX_") { - t.Errorf("invalid env var: %s", env) - } - if env == "LOCKBOX_CONFIG_TOML" { - continue - } - t.Setenv(env, "test") - expect[env] = struct{}{} - } - if !found { - t.Errorf("no environment variables found?") - } - read := config.Environ() - if len(read) != len(expect) { - t.Errorf("invalid environment variable info: %d != %d", len(expect), len(read)) - } - for k := range expect { - found := false - for _, r := range read { - if r == fmt.Sprintf("%s=test", k) { - found = true - break - } - } - if !found { - t.Errorf("unable to find env: %s", k) - } - } -} - func TestCanColor(t *testing.T) { os.Clearenv() if can, _ := config.CanColor(); !can {