From 2e7dfc1887fd44ae1fa9407bdf0fe4ff936405fc Mon Sep 17 00:00:00 2001 From: Omid Astaraki Date: Mon, 16 Mar 2026 12:18:12 +0000 Subject: [PATCH] fix(config): show only last 4 characters of API key in config show MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the prefix-exposing mask (first4****last4) with ********last4 so the key prefix is never revealed in output. Short keys (≤ 4 chars) remain fully masked as ****, and absent keys continue to show . Closes #67 --- cmd/config/show.go | 6 ++- tests/config_show_mask_test.go | 97 ++++++++++++++++++++++++++++++++++ tests/config_test.go | 10 +++- 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 tests/config_show_mask_test.go diff --git a/cmd/config/show.go b/cmd/config/show.go index b77beab..26d2fc5 100644 --- a/cmd/config/show.go +++ b/cmd/config/show.go @@ -15,9 +15,11 @@ var configShowCmd = &cobra.Command{ apiKey := viper.GetString("seerr.api_key") if apiKey != "" { - masked := apiKey + // Show only the last 4 characters so the key is identifiable without + // exposing the prefix, which is the more sensitive portion. + var masked string if len(apiKey) > 4 { - masked = apiKey[:4] + "****" + apiKey[len(apiKey)-4:] + masked = "********" + apiKey[len(apiKey)-4:] } else { masked = "****" } diff --git a/tests/config_show_mask_test.go b/tests/config_show_mask_test.go new file mode 100644 index 0000000..dbec027 --- /dev/null +++ b/tests/config_show_mask_test.go @@ -0,0 +1,97 @@ +package tests + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "seerr-cli/cmd" + + "github.com/spf13/viper" +) + +// writeConfigFile writes minimal YAML to a temp file so config show can read it. +func writeConfigFile(t *testing.T, content string) string { + t.Helper() + dir := t.TempDir() + p := filepath.Join(dir, ".seerr-cli.yaml") + if err := os.WriteFile(p, []byte(content), 0o600); err != nil { + t.Fatalf("writeConfigFile: %v", err) + } + return p +} + +func runConfigShow(t *testing.T, configPath string) string { + t.Helper() + viper.Reset() + b := new(bytes.Buffer) + cmd.RootCmd.SetOut(b) + cmd.RootCmd.SetErr(b) + cmd.RootCmd.SetArgs([]string{"config", "show", "--config", configPath}) + if err := cmd.RootCmd.Execute(); err != nil { + t.Fatalf("config show: %v", err) + } + return b.String() +} + +func TestConfigShowAPIKeyMasking(t *testing.T) { + tests := []struct { + name string + apiKey string + wantContain string + wantAbsent string + }{ + { + name: "long key shows only last 4 chars", + apiKey: "abcdefghijklmnop", + wantContain: "********mnop", + wantAbsent: "abcd", + }, + { + name: "exactly 5 char key shows only last 4", + apiKey: "hello", + wantContain: "****", + // "hell" must not appear + wantAbsent: "hell", + }, + { + name: "4 char key is fully masked", + apiKey: "1234", + wantContain: "****", + wantAbsent: "1234", + }, + { + name: "short key (2 chars) is fully masked", + apiKey: "ab", + wantContain: "****", + wantAbsent: "ab", + }, + { + name: "key absent shows not-set label", + apiKey: "", + wantContain: "", + wantAbsent: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var yaml string + if tc.apiKey != "" { + yaml = "seerr:\n server: http://localhost:5055\n api_key: " + tc.apiKey + "\n" + } else { + yaml = "seerr:\n server: http://localhost:5055\n" + } + p := writeConfigFile(t, yaml) + out := runConfigShow(t, p) + if !strings.Contains(out, tc.wantContain) { + t.Errorf("expected output to contain %q, got: %s", tc.wantContain, out) + } + if tc.wantAbsent != "" && strings.Contains(out, tc.wantAbsent) { + t.Errorf("expected output NOT to contain %q, got: %s", tc.wantAbsent, out) + } + }) + } +} diff --git a/tests/config_test.go b/tests/config_test.go index 74f4353..6652c91 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -91,10 +91,16 @@ func TestConfigCommands(t *testing.T) { if !strings.Contains(out, "http://test-server:5055") { t.Errorf("expected output to contain server URL, got: %s", out) } - // Check for masked API key - if !strings.Contains(out, "test****2345") { + // Only the last 4 characters should be visible; the prefix is masked. + if !strings.Contains(out, "********2345") { t.Errorf("expected output to contain masked API key, got: %s", out) } + // The API Key line must not contain the plain-text prefix of the key. + for _, line := range strings.Split(out, "\n") { + if strings.HasPrefix(line, "API Key:") && strings.Contains(line, "test-api") { + t.Errorf("expected API key prefix to be masked in line: %s", line) + } + } }) t.Run("config show empty", func(t *testing.T) {