feat(show): add configurable fuzzy-search fallback toggle (#3449)
* feat(show): add configurable fuzzy-search fallback toggle * fix(tests): update TestMountConfig expected output for show.fuzzysearch default Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> --------- Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
* [FEATURE] Add stable structured exit codes; --exit-codes flag lists all codes (I-2)
|
||||
* [FEATURE] Add JSON output to audit, list, find, and recipients commands (I-3)
|
||||
* [FEATURE] Add show.hidden-keys config option for customizable safecontent redaction (I-5)
|
||||
* [FEATURE] Add show.fuzzysearch config and --nofuzzysearch flag to control automatic fuzzy lookup in show
|
||||
* [FEATURE] Add --stdin, --file, and --exec modes to gopass env
|
||||
* [ENHANCEMENT] Unified secret name validation rejects path traversal and consecutive slashes (I-1)
|
||||
* [CLEANUP] Split Action handler into focused handler types (A-1)
|
||||
|
||||
@@ -30,6 +30,7 @@ Flag | Aliases | Description
|
||||
`--password` | `-o` | Display only the password. For use in scripts. Takes precedence over other flags.
|
||||
`--revision` | `-r` | Display a specific revision of the entry. Use an exact version identifier from `gopass history` or the special `-<N>` syntax. Does not work with native (e.g. git) refs.
|
||||
`--noparsing` | `-n` | Do not parse the content, disable YAML and Key-Value functions.
|
||||
`--nofuzzysearch` | | Do not start fuzzy search if the requested entry is not found.
|
||||
`--nosync` | | Disable auto-sync for this invocation.
|
||||
`--chars` | | Display selected characters from the password.
|
||||
|
||||
@@ -50,6 +51,7 @@ Note: The parser ensures every parsed secret contains a terminating newline, eve
|
||||
* The `--clip` flag will copy the value of the `Password` field to the clipboard and doesn't display any part of the secret.
|
||||
* The `--alsoclip` option will copy the value of the `Password` field but also display the secret content depending on the `safecontent` setting, i.e. obstructing the `Password` field if `safecontent` is `true` or just displaying it if not.
|
||||
* The `--qr` flags operates complementary to other flags. It will *additionally* format the value of the `Password` entry as a QR code and display it. Other than that it will honor the other options, e.g. `gopass show --qr` will display the QR code *and* the whole secret content below. One special case is the `-o` flag, this flag doesn't make a lot of sense in combination, so if both `--qr` and `-o` are given only the QR code will be displayed.
|
||||
* When an entry is not found, `gopass show` can start an interactive fuzzy search by default. This can be disabled globally with `show.fuzzysearch=false` or for one invocation via `--nofuzzysearch`.
|
||||
* Since gopass plans to supports different RCS backends we do not support arbitrary git refs as arguments to the `--revision` flag. Using those might work, but this is explicitly not supported and bug reports will be closed as `wont-fix`. There are two issues with using arbitrary git refs is that (a) this doesn't work with non-git RCS backends and (b) git versions a whole repository, not single files. So the revision `HEAD^`
|
||||
might not have any changes for a given entry. Thus we only support specifc revisions obtained from `gopass history` or our custom syntax `-N` where N is an integer identifying a specific commit before `HEAD` (cf. `HEAD~N`).
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ This is a list of available options:
|
||||
| `recipients.check` | `bool` | Check recipients hash. The global config option takes precedence over local ones here for security reasons. | `false` |
|
||||
| `recipients.hash` | `string` | SHA256 hash of the recipients file. Used to notify the user when the recipients files change. Not set, nor read at the local level for security reasons. | `` |
|
||||
| `show.autoclip` | `bool` | Autoclip in `gopass show` by default. | `false` |
|
||||
| `show.fuzzysearch` | `bool` | Automatically start fuzzy search in `gopass show` when an entry is not found. | `true` |
|
||||
| `show.post-hook` | `string` | This hook is run right after displaying a secret with `gopass show`. | `None` |
|
||||
| `show.safecontent` | `bool` | Only output _safe content_ (i.e. everything but the first line of a secret) to the terminal. Use _copy_ (`-c`) to retrieve the password in the clipboard, or _force_ (`-f`) to still print it. | `false` |
|
||||
| `show.hidden-keys` | `string` (repeatable) | Additional secret field names to redact when `show.safecontent` is enabled. Set this key multiple times to hide multiple fields. The built-in keys (`password`, `totp`, `hotp`, `otpauth`) are always hidden regardless of this setting. Example: `gopass config show.hidden-keys api_token` | *(none)* |
|
||||
|
||||
@@ -78,8 +78,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/gopasspw/clipboard v0.0.4 h1:v3HUlVHfBXPx9woIQnsBIbs9ZM3i77OCtVKRMLhmR+c=
|
||||
github.com/gopasspw/clipboard v0.0.4/go.mod h1:i0cShr7JEbOXZ/iKM5RyfBLbu1FPzouO8BTCJy0uHy8=
|
||||
github.com/gopasspw/clipboard v0.0.5-0.20260524141134-6b387ae5aa1a h1:L7qDnmetJ62Fr59V/8Rrhb5l1+TdrmxORyHJ/WNlwdg=
|
||||
github.com/gopasspw/clipboard v0.0.5-0.20260524141134-6b387ae5aa1a/go.mod h1:i0cShr7JEbOXZ/iKM5RyfBLbu1FPzouO8BTCJy0uHy8=
|
||||
github.com/gopasspw/gitconfig v0.0.4 h1:7JE0iTm92OdXCtkS33CnbqcAEqQXYWTUriYFf3sRTBk=
|
||||
@@ -240,8 +238,6 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
@@ -117,6 +117,10 @@ func ShowFlags() []cli.Flag {
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Do not parse the output.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nofuzzysearch",
|
||||
Usage: "Do not start fuzzy search when an entry is not found",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nosync",
|
||||
Usage: "Disable auto-sync",
|
||||
|
||||
@@ -52,7 +52,8 @@ core.notifications = true
|
||||
generate.autoclip = true
|
||||
`
|
||||
want += "mounts.path = " + fsutil.ShrinkPath(u.StoreDir("")) + "\n" +
|
||||
"pwgen.xkcd-lang = en\n"
|
||||
"pwgen.xkcd-lang = en\n" +
|
||||
"show.fuzzysearch = true\n"
|
||||
assert.Equal(t, want, buf.String())
|
||||
})
|
||||
|
||||
@@ -98,7 +99,8 @@ core.notifications = true
|
||||
generate.autoclip = true
|
||||
`
|
||||
want += "mounts.path = " + fsutil.ShrinkPath(u.StoreDir("")) + "\n" +
|
||||
"pwgen.xkcd-lang = en\n"
|
||||
"pwgen.xkcd-lang = en\n" +
|
||||
"show.fuzzysearch = true\n"
|
||||
|
||||
assert.Equal(t, want, buf.String(), "action.printConfigValues")
|
||||
})
|
||||
@@ -137,6 +139,7 @@ core.notifications
|
||||
generate.autoclip
|
||||
mounts.path
|
||||
pwgen.xkcd-lang
|
||||
show.fuzzysearch
|
||||
`
|
||||
assert.Equal(t, want, buf.String())
|
||||
})
|
||||
|
||||
@@ -48,6 +48,14 @@ func isTrailingFlag(arg string) bool {
|
||||
arg == "-C" || arg == "--alsoclip"
|
||||
}
|
||||
|
||||
func isShowFuzzySearchEnabled(ctx context.Context, cmd *cli.Command) bool {
|
||||
if cmd != nil && cmd.Bool("nofuzzysearch") {
|
||||
return false
|
||||
}
|
||||
|
||||
return config.Bool(ctx, "show.fuzzysearch")
|
||||
}
|
||||
|
||||
func showParseTrailingFlags(ctx context.Context, cmd *cli.Command) context.Context {
|
||||
for i := 1; i < cmd.Args().Len(); i++ {
|
||||
arg := cmd.Args().Get(i)
|
||||
@@ -472,7 +480,7 @@ func (s *secretHandler) hasAliasDomain(ctx context.Context, name string) string
|
||||
|
||||
// showHandleError handles errors retrieving secrets.
|
||||
func (s *secretHandler) showHandleError(ctx context.Context, cmd *cli.Command, name string, recurse bool, err error) error {
|
||||
if !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) {
|
||||
if !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) || !isShowFuzzySearchEnabled(ctx, cmd) {
|
||||
if IsClip(ctx) {
|
||||
_ = notify.Notify(ctx, "gopass - error", fmt.Sprintf("failed to retrieve secret %q: %s", name, err))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -10,11 +11,13 @@ import (
|
||||
"github.com/gopasspw/clipboard"
|
||||
"github.com/gopasspw/gopass/internal/config"
|
||||
"github.com/gopasspw/gopass/internal/out"
|
||||
"github.com/gopasspw/gopass/internal/store"
|
||||
"github.com/gopasspw/gopass/pkg/ctxutil"
|
||||
"github.com/gopasspw/gopass/pkg/gopass/secrets"
|
||||
"github.com/gopasspw/gopass/tests/gptest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestShowMulti(t *testing.T) {
|
||||
@@ -596,6 +599,51 @@ func TestShowHandleError(t *testing.T) {
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
func TestShowHandleErrorFuzzySearchToggle(t *testing.T) {
|
||||
u := gptest.NewUnitTester(t)
|
||||
|
||||
ctx := config.NewContextInMemory()
|
||||
ctx = ctxutil.WithAlwaysYes(ctx, true)
|
||||
ctx = ctxutil.WithTerminal(ctx, true)
|
||||
act, err := newMock(ctx, u.StoreDir(""))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, act)
|
||||
ctx = act.cfg.WithConfig(ctx)
|
||||
|
||||
called := false
|
||||
act.secrets.findFuzzyFn = func(context.Context, *cli.Command) error {
|
||||
called = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("default config enables fuzzy search", func(t *testing.T) {
|
||||
called = false
|
||||
c := gptest.CliCtx(ctx, t)
|
||||
|
||||
require.NoError(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("show.fuzzysearch=false disables fuzzy search", func(t *testing.T) {
|
||||
called = false
|
||||
require.NoError(t, act.cfg.Set("", "show.fuzzysearch", "false"))
|
||||
c := gptest.CliCtx(ctx, t)
|
||||
|
||||
require.Error(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
|
||||
assert.False(t, called)
|
||||
})
|
||||
|
||||
t.Run("--nofuzzysearch disables fuzzy search for one invocation", func(t *testing.T) {
|
||||
called = false
|
||||
require.NoError(t, act.cfg.Set("", "show.fuzzysearch", "true"))
|
||||
c := gptest.CliCtxWithFlags(ctx, t, map[string]string{"nofuzzysearch": "true"})
|
||||
|
||||
require.Error(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
|
||||
assert.False(t, called)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShowPrintQR(t *testing.T) {
|
||||
u := gptest.NewUnitTester(t)
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ var defaults = map[string]string{
|
||||
"core.notifications": "true",
|
||||
"core.follow-references": "false",
|
||||
"pwgen.xkcd-lang": "en",
|
||||
"show.fuzzysearch": "true",
|
||||
}
|
||||
|
||||
// Config is a gopass config handler.
|
||||
|
||||
@@ -47,6 +47,7 @@ func TestConfig(t *testing.T) {
|
||||
"env.string",
|
||||
"mounts.path",
|
||||
"pwgen.xkcd-lang",
|
||||
"show.fuzzysearch",
|
||||
}, cfg.Keys(""))
|
||||
for key, expected := range defaults {
|
||||
assert.Equal(t, expected, cfg.Get(key))
|
||||
|
||||
@@ -27,7 +27,8 @@ core.follow-references = false
|
||||
core.notifications = true
|
||||
`
|
||||
wanted += "mounts.path = " + ts.storeDir("root") + "\n" +
|
||||
"pwgen.xkcd-lang = en"
|
||||
`pwgen.xkcd-lang = en
|
||||
show.fuzzysearch = true`
|
||||
|
||||
assert.Equal(t, wanted, out)
|
||||
|
||||
@@ -86,6 +87,7 @@ core.notifications = true
|
||||
wanted += "mounts.path = " + ts.storeDir("root") + "\n"
|
||||
wanted += "pwgen.xkcd-lang = en\n"
|
||||
wanted += "recipients.mnt/m1.hash = 9a4c4b1e0eb9ade2e692ff948f43d9668145eca3df88ffff67e0e21426252907\n"
|
||||
wanted += "show.fuzzysearch = true\n"
|
||||
|
||||
out, err := ts.run("config")
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user