diff --git a/plugins/pass/commands/run.go b/plugins/pass/commands/run.go index 85a45860..976e334f 100644 --- a/plugins/pass/commands/run.go +++ b/plugins/pass/commands/run.go @@ -258,10 +258,6 @@ func parseEnv(env []string) ([]envVar, error) { key, value, _ := strings.Cut(kv, "=") v := envVar{key: key, value: value} if rawSeRef, ok := strings.CutPrefix(value, sePrefix); ok { - // ParseID rejects wildcards before ParsePattern broadens the lookup. - if _, err := secrets.ParseID(rawSeRef); err != nil { - return nil, fmt.Errorf("resolving %s: %w", key, err) - } pattern, err := secrets.ParsePattern(rawSeRef) if err != nil { return nil, fmt.Errorf("resolving %s: %w", key, err) @@ -317,8 +313,7 @@ func resolveVar(ctx context.Context, r secrets.Resolver, v envVar) (string, erro if len(envs) == 0 { return "", fmt.Errorf("resolving %s: %w", v.key, secrets.ErrNotFound) } - if len(envs) > 1 { - return "", fmt.Errorf("resolving %s: %d secrets matched %s", v.key, len(envs), v.pattern) - } + // TODO: Extend the query language + // For now: First match wins. return string(envs[0].Value), nil } diff --git a/plugins/pass/commands/run_example.md b/plugins/pass/commands/run_example.md index dc969034..414b0134 100644 --- a/plugins/pass/commands/run_example.md +++ b/plugins/pass/commands/run_example.md @@ -10,6 +10,12 @@ $ SE_TOKEN=se://gh-token docker pass run -- gh repo list $ DB_PASSWORD=se://myapp/postgres/password API_KEY=se://myapp/anthropic/api-key docker pass run -- ./my-binary ``` +### Wildcard pattern (the first matching secret is used): + +```console +$ DB_PASSWORD=se://*/postgres/password docker pass run -- ./my-binary +``` + ### Resolve references from a dotenv file: ```console diff --git a/plugins/pass/commands/run_long.md b/plugins/pass/commands/run_long.md index 0a38cd68..ef8209b5 100644 --- a/plugins/pass/commands/run_long.md +++ b/plugins/pass/commands/run_long.md @@ -1,6 +1,7 @@ Scans the current environment (plus any `--env-file` inputs) for variables whose value is exactly `se://`. Each reference is resolved through the secrets-engine daemon and the resolved value is passed to the child process. +When several secrets match, the first match is used. The child inherits stdin, stdout, and stderr. Requires the secrets-engine daemon (Docker Desktop) to be running. diff --git a/plugins/pass/commands/run_test.go b/plugins/pass/commands/run_test.go index de758860..039673d8 100644 --- a/plugins/pass/commands/run_test.go +++ b/plugins/pass/commands/run_test.go @@ -160,16 +160,18 @@ func TestParseEnv(t *testing.T) { assert.Equal(t, []envVar{{key: "DSN", value: "postgres://user:se://gh-token@host/db"}}, vars) }) - t.Run("invalid ID hard-fails", func(t *testing.T) { - vars, err := parseEnv([]string{"X=se://"}) - require.Error(t, err) - assert.Nil(t, vars) - assert.ErrorContains(t, err, "resolving X") + t.Run("wildcard reference carries its pattern", func(t *testing.T) { + vars, err := parseEnv([]string{"X=se://foo/*", "Y=se://**/token"}) + require.NoError(t, err) + assert.Equal(t, []envVar{ + {key: "X", value: "se://foo/*", pattern: secrets.MustParsePattern("foo/*")}, + {key: "Y", value: "se://**/token", pattern: secrets.MustParsePattern("**/token")}, + }, vars) }) - t.Run("wildcard in reference is rejected", func(t *testing.T) { - vars, err := parseEnv([]string{"X=se://foo/*"}) - require.Error(t, err) + t.Run("invalid pattern hard-fails", func(t *testing.T) { + vars, err := parseEnv([]string{"X=se://"}) + require.ErrorIs(t, err, secrets.ErrInvalidPattern) assert.Nil(t, vars) assert.ErrorContains(t, err, "resolving X") }) @@ -190,6 +192,7 @@ func TestResolveEnv(t *testing.T) { Store: map[secrets.ID]string{ secrets.MustParseID("gh-token"): "ghp_abc123", secrets.MustParseID("myapp/postgres/password"): "s3cr3t", + secrets.MustParseID("other/postgres/password"): "0th3r", }, } @@ -214,6 +217,13 @@ func TestResolveEnv(t *testing.T) { assert.Equal(t, []string{"PG_PWD=s3cr3t"}, out) }) + t.Run("wildcard resolves to the first match", func(t *testing.T) { + in := []string{"PG_PWD=se://*/postgres/password"} + out, err := resolveEnv(t.Context(), mock, mustParseEnv(t, in)) + require.NoError(t, err) + assert.Equal(t, []string{"PG_PWD=s3cr3t"}, out) + }) + t.Run("missing reference hard-fails", func(t *testing.T) { in := []string{"X=se://does-not-exist"} out, err := resolveEnv(t.Context(), mock, mustParseEnv(t, in))