-
-
Notifications
You must be signed in to change notification settings - Fork 134
feat(cache): migrate API key helper cache to OS credential store #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package util | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
|
|
@@ -10,6 +11,16 @@ import ( | |
| "time" | ||
| ) | ||
|
|
||
| // overrideCredStore replaces the global credStore with an isolated file-backed | ||
| // store backed by a temp directory, then restores the original on cleanup. | ||
| // This prevents tests from touching the OS keyring or real credential files. | ||
| func overrideCredStore(t *testing.T) { | ||
| t.Helper() | ||
| original := credStore | ||
| t.Cleanup(func() { credStore = original }) | ||
| credStore = newTestCredStore(t) | ||
| } | ||
|
|
||
| func TestGetAPIKeyFromHelper_Success(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
|
|
@@ -207,6 +218,7 @@ func TestGetAPIKeyFromHelper_SecurityStderr(t *testing.T) { | |
| } | ||
|
|
||
| func TestGetAPIKeyFromHelperWithCache_NoCaching(t *testing.T) { | ||
| overrideCredStore(t) | ||
| // Test with refreshInterval = 0 (no caching) | ||
| command := "echo 'test-key-no-cache'" | ||
|
|
||
|
|
@@ -229,13 +241,9 @@ func TestGetAPIKeyFromHelperWithCache_NoCaching(t *testing.T) { | |
| } | ||
|
|
||
| func TestGetAPIKeyFromHelperWithCache_WithCaching(t *testing.T) { | ||
| // Create a temporary directory for testing | ||
| overrideCredStore(t) | ||
| // Use a counter file to generate different values each time the command runs. | ||
| tmpDir := t.TempDir() | ||
|
|
||
| // Override home directory for testing | ||
| t.Setenv("HOME", tmpDir) | ||
|
|
||
| // Use a counter file to generate different values each time the command runs | ||
| counterFile := filepath.Join(tmpDir, "counter.txt") | ||
|
Comment on lines
243
to
247
|
||
| command := fmt.Sprintf( | ||
| "f=%s; echo $(($(cat $f 2>/dev/null || echo 0) + 1)) | tee $f", | ||
|
|
@@ -263,13 +271,9 @@ func TestGetAPIKeyFromHelperWithCache_WithCaching(t *testing.T) { | |
| } | ||
|
|
||
| func TestGetAPIKeyFromHelperWithCache_CacheExpiration(t *testing.T) { | ||
| // Create a temporary directory for testing | ||
| overrideCredStore(t) | ||
| // Create a counter file that we'll update manually. | ||
| tmpDir := t.TempDir() | ||
|
|
||
| // Override home directory for testing | ||
| t.Setenv("HOME", tmpDir) | ||
|
|
||
| // Create a counter file that we'll update manually | ||
| counterFile := filepath.Join(tmpDir, "counter2.txt") | ||
| command := fmt.Sprintf("cat %q", counterFile) | ||
|
|
||
|
|
@@ -311,12 +315,7 @@ func TestGetAPIKeyFromHelperWithCache_CacheExpiration(t *testing.T) { | |
| } | ||
|
|
||
| func TestGetAPIKeyFromHelperWithCache_DifferentCommands(t *testing.T) { | ||
| // Create a temporary directory for testing | ||
| tmpDir := t.TempDir() | ||
|
|
||
| // Override home directory for testing | ||
| t.Setenv("HOME", tmpDir) | ||
|
|
||
| overrideCredStore(t) | ||
| cmd1 := "echo 'key-one'" | ||
| cmd2 := "echo 'key-two'" | ||
|
|
||
|
|
@@ -344,35 +343,31 @@ func TestGetAPIKeyFromHelperWithCache_DifferentCommands(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestGetAPIKeyFromHelperWithCache_CacheFilePermissions(t *testing.T) { | ||
| // Create a temporary directory for testing | ||
| tmpDir := t.TempDir() | ||
|
|
||
| // Override home directory for testing | ||
| t.Setenv("HOME", tmpDir) | ||
|
|
||
| command := "echo 'test-permissions'" | ||
| func TestGetAPIKeyFromHelperWithCache_StoresInCredstore(t *testing.T) { | ||
| overrideCredStore(t) | ||
| command := "echo 'test-credstore-storage'" | ||
|
|
||
| // Execute command to create cache file | ||
| // Execute command to populate credstore cache | ||
| _, err := GetAPIKeyFromHelperWithCache(context.Background(), command, 5*time.Second) | ||
|
Comment on lines
+346
to
351
|
||
| if err != nil { | ||
| t.Fatalf("GetAPIKeyFromHelperWithCache() error = %v", err) | ||
| } | ||
|
|
||
| // Check cache file permissions | ||
| cachePath, err := getCacheFilePath(command) | ||
| // Verify the value was stored in credstore under the expected key | ||
| val, err := GetCredential(helperCacheKey(command)) | ||
| if err != nil { | ||
| t.Fatalf("getCacheFilePath() error = %v", err) | ||
| t.Fatalf("GetCredential() error = %v", err) | ||
| } | ||
|
|
||
| info, err := os.Stat(cachePath) | ||
| if err != nil { | ||
| t.Fatalf("os.Stat() error = %v", err) | ||
| if val == "" { | ||
| t.Fatal("Expected credstore to contain cached entry, got empty string") | ||
| } | ||
|
|
||
| // Check that file has restrictive permissions (0600) | ||
| perm := info.Mode().Perm() | ||
| if perm != 0o600 { | ||
| t.Errorf("Cache file should have 0600 permissions, got %o", perm) | ||
| // Verify the stored JSON contains the expected API key | ||
| var stored apiKeyCache | ||
| if err := json.Unmarshal([]byte(val), &stored); err != nil { | ||
| t.Fatalf("Failed to unmarshal stored credstore value: %v", err) | ||
| } | ||
| if stored.APIKey != "test-credstore-storage" { | ||
| t.Errorf("Stored APIKey = %q, want %q", stored.APIKey, "test-credstore-storage") | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
writeCachepersists the fullhelperCmdstring inside the cached JSON (HelperCmd: helperCmd). Since the credstore key is already derived from the helper command hash, storing the raw command is redundant and can unintentionally persist sensitive data if the command embeds secrets (especially in the file-fallback backend). Consider removingHelperCmdfrom the cached payload (or storing only a hash) and dropping the equality check inreadCache.