lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 498f77791bffdab3f45ec2efb34acdb2661962ff
parent c189636a51b1c771de493c2043af35c284d8732c
Author: Sean Enck <sean@ttypty.com>
Date:   Sat, 14 Jun 2025 11:23:14 -0400

switch to feature configs

Diffstat:
MREADME.md | 12------------
Mcmd/lb/main_test.go | 17-----------------
Mcmd/lb/tests/expected.log | 5-----
Minternal/app/completions/core.go | 6+++---
Minternal/app/completions/core_test.go | 27+++++++++++++++++++++++++++
Minternal/app/help/core.go | 13++++++++++---
Minternal/app/help/core_test.go | 22+++++++++++++++++++++-
Minternal/app/help/doc/clipboard.txt | 3++-
Minternal/app/help/doc/totp.txt | 2+-
Minternal/app/totp.go | 5++---
Minternal/app/totp_test.go | 5+++++
Minternal/config/core.go | 13++++++++++++-
Minternal/config/core_test.go | 17+++++++++++++++--
Dinternal/config/features/clip.go | 7-------
Dinternal/config/features/flags.go | 24------------------------
Dinternal/config/features/flags_test.go | 26--------------------------
Dinternal/config/features/totp.go | 7-------
Minternal/config/toml_test.go | 2+-
Minternal/config/vars.go | 24++++++++++++++++++++++++
Minternal/config/vars_test.go | 12++++++++++++
Minternal/platform/clip/core.go | 5++---
Minternal/platform/clip/core_test.go | 8++++++++
Mjustfile | 10++--------
23 files changed, 147 insertions(+), 125 deletions(-)

diff --git a/README.md b/README.md @@ -127,15 +127,3 @@ just ``` _run `just check` to run tests_ - -### features - -`lb` can have some features disabled via build tags: - -- 'noclip' will disable the clipboard functionalities -- 'nototp' will disable the totp functionalities - -These are supported via the justfile -``` -just tags=noclip -``` diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -105,12 +105,6 @@ func (r runner) raw(pipeIn, command, stdout, stderr string) error { return cmd.Run() } -func (r runner) feature(command, useBinary, grep string) error { - text := fmt.Sprintf("%s %s %s >> %s 2>&1", fmt.Sprintf("%s-%s", binary, useBinary), command, grep, r.log) - cmd := exec.Command("/bin/sh", "-c", text) - return cmd.Run() -} - func (r runner) logAppend(command string) error { return exec.Command("/bin/sh", "-c", fmt.Sprintf("%s >> %s", command, r.log)).Run() } @@ -355,17 +349,6 @@ func test(profile string) error { r.section("env") r.run("", fmt.Sprintf("vars | sed 's#/%s#/datadir#g' | grep -v CREDENTIALS | sort", profile)) - r.section("features") - feature := func(cmd, flag string) { - executable := fmt.Sprintf("no%s", flag) - for _, e := range []string{executable, "nofeatures"} { - r.feature(cmd, e, "") - r.feature("help verbose", e, fmt.Sprintf("| grep '%s'", flag)) - } - } - feature("clip abc", "clip") - feature("totp ls", "totp") - // cleanup and diff results tmpFile := fmt.Sprintf("%s.tmp", r.log) for _, item := range []string{"'s/\"modtime\": \"[0-9].*$/\"modtime\": \"XXXX-XX-XX\",/g'", "'s/^[0-9][0-9][0-9][0-9][0-9][0-9]$/XXXXXX/g'"} { diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -330,8 +330,3 @@ LOCKBOX_JSON_HASH_LENGTH=3 LOCKBOX_JSON_MODE=hash LOCKBOX_STORE=testdata/datadir/pass.kdbx LOCKBOX_TOTP_TIMEOUT=1 -features -unable to get clipboard: clip feature is disabled -unable to get clipboard: clip feature is disabled -totp feature is disabled -totp feature is disabled diff --git a/internal/app/completions/core.go b/internal/app/completions/core.go @@ -11,7 +11,7 @@ import ( "text/template" "git.sr.ht/~enckse/lockbox/internal/app/commands" - "git.sr.ht/~enckse/lockbox/internal/config/features" + "git.sr.ht/~enckse/lockbox/internal/config" ) type ( @@ -69,11 +69,11 @@ func Generate(completionType, exe string) ([]string, error) { } c.Options = []string{commands.Help, commands.List, commands.Show, commands.Version, commands.JSON, commands.Groups, commands.Move, commands.Remove, commands.Insert, commands.Unset} - canClip := features.CanClip() + canClip := config.EnvFeatureClip.Get() if canClip { c.Options = append(c.Options, commands.Clip) } - if features.CanTOTP() { + if config.EnvFeatureTOTP.Get() { c.Options = append(c.Options, commands.TOTP) c.TOTPSubCommands = []string{commands.TOTPMinimal, commands.TOTPOnce, commands.TOTPShow, commands.TOTPURL, commands.TOTPSeed} if canClip { diff --git a/internal/app/completions/core_test.go b/internal/app/completions/core_test.go @@ -1,10 +1,12 @@ package completions_test import ( + "fmt" "strings" "testing" "git.sr.ht/~enckse/lockbox/internal/app/completions" + "git.sr.ht/~enckse/lockbox/internal/config/store" ) func TestCompletions(t *testing.T) { @@ -16,7 +18,19 @@ func TestCompletions(t *testing.T) { } } +func testCompletionFeature(t *testing.T, completionMode, cmd string, expect int) { + e := expect + if cmd == "totp" && completionMode == "bash" { + e++ + } + v, _ := completions.Generate(completionMode, "lb") + if cnt := strings.Count(strings.Join(v, "\n"), cmd); cnt != e { + t.Errorf("completion mismatch %s: %d != %d (%s)", completionMode, cnt, expect, cmd) + } +} + func testCompletion(t *testing.T, completionMode, need string) { + defer store.Clear() v, err := completions.Generate(completionMode, "lb") if err != nil { t.Errorf("invalid error: %v", err) @@ -27,4 +41,17 @@ func testCompletion(t *testing.T, completionMode, need string) { if !strings.Contains(v[0], need) { t.Errorf("invalid output, bad shell generation: %v", v) } + type counts struct { + cmd string + with int + without int + } + for _, feature := range []counts{{"clip", 5, 1}, {"totp", 9, 1}} { + store.Clear() + key := fmt.Sprintf("LOCKBOX_FEATURE_%s", strings.ToUpper(feature.cmd)) + store.SetBool(key, true) + testCompletionFeature(t, completionMode, feature.cmd, feature.with) + store.SetBool(key, false) + testCompletionFeature(t, completionMode, feature.cmd, feature.without) + } } diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -12,7 +12,6 @@ import ( "git.sr.ht/~enckse/lockbox/internal/app/commands" "git.sr.ht/~enckse/lockbox/internal/config" - "git.sr.ht/~enckse/lockbox/internal/config/features" "git.sr.ht/~enckse/lockbox/internal/kdbx" "git.sr.ht/~enckse/lockbox/internal/output" ) @@ -36,6 +35,7 @@ type ( CompletionsEnv string HelpCommand string HelpConfigCommand string + NoColor string Config struct { Env string Home string @@ -76,7 +76,7 @@ func Usage(verbose bool, exe string) ([]string, error) { isGroup = "group" ) var results []string - canClip := features.CanClip() + canClip := config.EnvFeatureClip.Get() if canClip { results = append(results, command(commands.Clip, isEntry, "copy the entry's value into the clipboard")) } @@ -97,7 +97,7 @@ func Usage(verbose bool, exe string) ([]string, error) { 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.Show, isEntry, "show the entry's value")) - canTOTP := features.CanTOTP() + canTOTP := config.EnvFeatureTOTP.Get() if canTOTP { results = append(results, command(commands.TOTP, isEntry, "display an updating totp generated code")) if canClip { @@ -114,6 +114,7 @@ func Usage(verbose bool, exe string) ([]string, error) { sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} if verbose { + canColor := config.EnvFeatureColor.Get() results = append(results, "") document := Documentation{ Executable: filepath.Base(exe), @@ -123,6 +124,7 @@ func Usage(verbose bool, exe string) ([]string, error) { CompletionsCommand: commands.Completions, HelpCommand: commands.Help, HelpConfigCommand: commands.HelpConfig, + NoColor: config.NoColorFlag, } document.Config.Env = config.ConfigEnv document.Config.Home = config.ConfigHome @@ -161,6 +163,11 @@ func Usage(verbose bool, exe string) ([]string, error) { if !canClip { continue } + case "color": + if !canColor { + continue + } + } header := fmt.Sprintf("[%s]", section) s, err := processDoc(header, n, document) diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -1,10 +1,12 @@ package help_test import ( + "fmt" "strings" "testing" "git.sr.ht/~enckse/lockbox/internal/app/help" + "git.sr.ht/~enckse/lockbox/internal/config/store" ) func TestUsage(t *testing.T) { @@ -13,7 +15,7 @@ func TestUsage(t *testing.T) { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 112 { + if len(u) != 117 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { @@ -24,3 +26,21 @@ func TestUsage(t *testing.T) { } } } + +func TestFlags(t *testing.T) { + defer store.Clear() + for _, feature := range []string{"clip", "totp", "color"} { + store.Clear() + key := fmt.Sprintf("LOCKBOX_FEATURE_%s", strings.ToUpper(feature)) + store.SetBool(key, true) + u, _ := help.Usage(true, "lb") + if !strings.Contains(strings.Join(u, "\n"), feature) { + t.Errorf("verbose help lacks: %s", feature) + } + store.SetBool(key, false) + u, _ = help.Usage(true, "lb") + if strings.Contains(strings.Join(u, "\n"), feature) { + t.Errorf("verbose help has: %s", feature) + } + } +} diff --git a/internal/app/help/doc/clipboard.txt b/internal/app/help/doc/clipboard.txt @@ -1,3 +1,4 @@ By default clipboard commands are detected via determing the platform and utilizing default commands to interact with (copy to/paste to) the clipboard. -These settings can be overriden via configuration. +These settings can be overriden via configuration. This feature can be +controlled by a configuration feature flag. diff --git a/internal/app/help/doc/totp.txt b/internal/app/help/doc/totp.txt @@ -1,3 +1,3 @@ By default '{{ $.Executable }}' tries to use some reasonable defaults to setup/manage oauth token inputs and displaying of code outputs. Many of these settings can be -changed via configuration. +changed via configuration. This feature can be controlled by a configuration feature flag. diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -14,7 +14,6 @@ import ( "git.sr.ht/~enckse/lockbox/internal/app/commands" "git.sr.ht/~enckse/lockbox/internal/config" - "git.sr.ht/~enckse/lockbox/internal/config/features" "git.sr.ht/~enckse/lockbox/internal/kdbx" "git.sr.ht/~enckse/lockbox/internal/platform/clip" ) @@ -209,8 +208,8 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error { // NewTOTPArguments will parse the input arguments func NewTOTPArguments(args []string) (*TOTPArguments, error) { - if !features.CanTOTP() { - return nil, features.NewError("totp") + if !config.EnvFeatureTOTP.Get() { + return nil, config.NewFeatureError("totp") } if len(args) == 0 { return nil, errors.New("not enough arguments for totp") diff --git a/internal/app/totp_test.go b/internal/app/totp_test.go @@ -82,6 +82,11 @@ func TestNewTOTPArgumentsErrors(t *testing.T) { if _, err := app.NewTOTPArguments([]string{"show"}); err == nil || err.Error() != "invalid arguments" { t.Errorf("invalid error: %v", err) } + defer store.Clear() + store.SetBool("LOCKBOX_FEATURE_TOTP", false) + if _, err := app.NewTOTPArguments([]string{"show"}); err == nil || err.Error() != "totp feature is disabled" { + t.Errorf("invalid error: %v", err) + } } func TestNewTOTPArguments(t *testing.T) { diff --git a/internal/config/core.go b/internal/config/core.go @@ -16,6 +16,7 @@ import ( const ( // sub categories + featureCategory = "FEATURE_" clipCategory = "CLIP_" totpCategory = "TOTP_" jsonCategory = "JSON_" @@ -33,6 +34,8 @@ const ( arrayDelimiter = " " // TimeWindowSpan indicates the delineation between start -> end (start:end) TimeWindowSpan = ":" + // NoColorFlag is the common color disable flag + NoColorFlag = "NO_COLOR" ) const ( @@ -150,7 +153,10 @@ func formatterTOTP(key, value string) string { // CanColor indicates if colorized output is allowed (or disabled) func CanColor() bool { - if _, noColor := os.LookupEnv("NO_COLOR"); noColor { + if !EnvFeatureColor.Get() { + return false + } + if _, noColor := os.LookupEnv(NoColorFlag); noColor { return false } return true @@ -173,3 +179,8 @@ func readNested(v reflect.Type, root string) []string { func TextPositionFields() string { return strings.Join(readNested(reflect.TypeOf(Word{}), ""), ", ") } + +// NewFeatureError creates an error if a feature is not enabled +func NewFeatureError(name string) error { + return fmt.Errorf("%s feature is disabled", name) +} diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -37,11 +37,17 @@ func TestNewEnvFiles(t *testing.T) { func TestCanColor(t *testing.T) { store.Clear() - if can := config.CanColor(); !can { + defer store.Clear() + if !config.CanColor() { t.Error("should be able to color") } + store.SetBool("LOCKBOX_FEATURE_COLOR", false) + if config.CanColor() { + t.Error("should NOT be able to color") + } + store.Clear() t.Setenv("NO_COLOR", "1") - if can := config.CanColor(); can { + if config.CanColor() { t.Error("should NOT be able to color") } } @@ -52,3 +58,10 @@ func TestTextFields(t *testing.T) { t.Errorf("unexpected fields: %s", v) } } + +func TestNewFeatureError(t *testing.T) { + err := config.NewFeatureError("abc") + if err == nil || err.Error() != "abc feature is disabled" { + t.Errorf("invalid error: %v", err) + } +} diff --git a/internal/config/features/clip.go b/internal/config/features/clip.go @@ -1,7 +0,0 @@ -//go:build noclip - -package features - -func init() { - clipFeature = false -} diff --git a/internal/config/features/flags.go b/internal/config/features/flags.go @@ -1,24 +0,0 @@ -// Package features controls build flag features -package features - -import "fmt" - -var ( - clipFeature = true - totpFeature = true -) - -// CanTOTP indicates if TOTP is enabled -func CanTOTP() bool { - return totpFeature -} - -// CanClip indicates if clip(board) is enabled -func CanClip() bool { - return clipFeature -} - -// NewError creates an error if a feature is not enabled -func NewError(name string) error { - return fmt.Errorf("%s feature is disabled", name) -} diff --git a/internal/config/features/flags_test.go b/internal/config/features/flags_test.go @@ -1,26 +0,0 @@ -package features_test - -import ( - "testing" - - "git.sr.ht/~enckse/lockbox/internal/config/features" -) - -func TestCanTOTP(t *testing.T) { - if !features.CanTOTP() { - t.Error("flag should be on by default") - } -} - -func TestCanClip(t *testing.T) { - if !features.CanClip() { - t.Error("flag should be on by default") - } -} - -func TestNewError(t *testing.T) { - err := features.NewError("abc") - if err == nil || err.Error() != "abc feature is disabled" { - t.Errorf("invalid error: %v", err) - } -} diff --git a/internal/config/features/totp.go b/internal/config/features/totp.go @@ -1,7 +0,0 @@ -//go:build nototp - -package features - -func init() { - totpFeature = false -} diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -251,7 +251,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) { if err := config.LoadConfigFile(file); err != nil { t.Errorf("invalid error: %v", err) } - if len(store.List()) != 16 { + if len(store.List()) != 19 { t.Errorf("invalid environment after load: %d", len(store.List())) } } diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -10,6 +10,30 @@ import ( ) var ( + // EnvFeatureTOTP allows disable TOTP feature + EnvFeatureTOTP = environmentRegister(EnvironmentBool{ + environmentDefault: newDefaultedEnvironment(true, + environmentBase{ + key: featureCategory + "TOTP", + description: "Enable totp feature.", + }), + }) + // EnvFeatureClip allows disabling clipboard feature + EnvFeatureClip = environmentRegister(EnvironmentBool{ + environmentDefault: newDefaultedEnvironment(true, + environmentBase{ + key: featureCategory + "CLIP", + description: "Enable clipboard feature.", + }), + }) + // EnvFeatureColor allows disabling color output + EnvFeatureColor = environmentRegister(EnvironmentBool{ + environmentDefault: newDefaultedEnvironment(true, + environmentBase{ + key: featureCategory + "COLOR", + description: "Enable terminal color feature.", + }), + }) // EnvClipTimeout gets the maximum clipboard time EnvClipTimeout = environmentRegister(EnvironmentInt{ environmentDefault: newDefaultedEnvironment(45, diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -28,6 +28,18 @@ func TestIsReadOnly(t *testing.T) { checkYesNo("LOCKBOX_READONLY", t, config.EnvReadOnly, false) } +func TestTOTPFeature(t *testing.T) { + checkYesNo("LOCKBOX_FEATURE_TOTP", t, config.EnvFeatureTOTP, true) +} + +func TestClipFeature(t *testing.T) { + checkYesNo("LOCKBOX_FEATURE_CLIP", t, config.EnvFeatureClip, true) +} + +func TestColorFeature(t *testing.T) { + checkYesNo("LOCKBOX_FEATURE_COLOR", t, config.EnvFeatureColor, true) +} + func TestIsOSC52(t *testing.T) { checkYesNo("LOCKBOX_CLIP_OSC52", t, config.EnvClipOSC52, false) } diff --git a/internal/platform/clip/core.go b/internal/platform/clip/core.go @@ -8,7 +8,6 @@ import ( "git.sr.ht/~enckse/lockbox/internal/app/commands" "git.sr.ht/~enckse/lockbox/internal/config" - "git.sr.ht/~enckse/lockbox/internal/config/features" "git.sr.ht/~enckse/lockbox/internal/platform" osc "github.com/aymanbagabas/go-osc52" ) @@ -33,8 +32,8 @@ func newBoard(copying, pasting []string) (Board, error) { // New creates a new clipboard func New() (Board, error) { - if !features.CanClip() { - return Board{}, features.NewError("clip") + if !config.EnvFeatureClip.Get() { + return Board{}, config.NewFeatureError("clip") } overridePaste := config.EnvClipPaste.Get() overrideCopy := config.EnvClipCopy.Get() diff --git a/internal/platform/clip/core_test.go b/internal/platform/clip/core_test.go @@ -8,6 +8,14 @@ import ( "git.sr.ht/~enckse/lockbox/internal/platform/clip" ) +func TestDisabled(t *testing.T) { + defer store.Clear() + store.SetBool("LOCKBOX_FEATURE_CLIP", false) + if _, err := clip.New(); err == nil || err.Error() != "clip feature is disabled" { + t.Errorf("invalid error: %v", err) + } +} + func TestMaxTime(t *testing.T) { store.Clear() defer store.Clear() diff --git a/justfile b/justfile @@ -6,27 +6,21 @@ ldflags := env_var_or_default("LDFLAGS", "") gotest := "LOCKBOX_CONFIG_TOML=fake go test" files := `find . -type f -name "*.go" | tr '\n' ' '` cmd := "cmd/lb" -tags := "" default: build build: mkdir -p "{{target}}" - go build {{goflags}} -tags={{tags}} -ldflags "{{ldflags}} -X main.version={{version}}" -o "{{object}}" {{cmd}}/main.go + go build {{goflags}} -ldflags "{{ldflags}} -X main.version={{version}}" -o "{{object}}" {{cmd}}/main.go unittest: {{gotest}} ./... check: unittest tests -tests: features +tests: build {{gotest}} {{cmd}}/main_test.go -features: build - just tags=noclip object={{target}}/lb-noclip - just tags=nototp object={{target}}/lb-nototp - just tags=noclip,nototp object={{target}}/lb-nofeatures - clean: rm -f "{{object}}" find internal/ {{cmd}} -type f -wholename "*testdata*" -delete