Add backend resolver interface with file and env backends plus phase-3 stubs

Introduce the single Backend interface every credential source implements.
v0 ships two working backends: file (0600 KEY=VALUE env files, parsed in
pure Go, never sourced; looser permission masks refused before read) and
env (process-environment indirection). Bitwarden Secrets Manager and
HashiCorp Vault ship as explicit not-implemented stubs behind the same
interface so the phase-3 connectors are drop-ins. All failures are typed
ResolveErrors carrying only the ref, backend, and a fixed reason enum —
never material.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-28 22:30:50 -05:00
parent 325a36cc35
commit b43bc55963
10 changed files with 730 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
package backend
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
const testMaterial = "sk-live-supersecret-material-0123456789"
// writeEnvFile writes an env file with the given permission mask.
func writeEnvFile(t *testing.T, dir, name, content string, mode os.FileMode) string {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, mode); err != nil {
t.Fatal(err)
}
return path
}
func TestFileResolve(t *testing.T) {
dir := t.TempDir()
secretFile := writeEnvFile(t, dir, "creds.env", "API_KEY="+testMaterial+"\nOTHER=x\n", 0o600)
emptyFile := writeEnvFile(t, dir, "empty.env", "API_KEY=\n", 0o600)
looseFile := writeEnvFile(t, dir, "loose.env", "API_KEY="+testMaterial+"\n", 0o644)
strictFile := writeEnvFile(t, dir, "strict.env", "API_KEY="+testMaterial+"\n", 0o400)
groupFile := writeEnvFile(t, dir, "group.env", "API_KEY="+testMaterial+"\n", 0o640)
brokenFile := writeEnvFile(t, dir, "broken.env", "this line has no equals\n", 0o600)
tests := []struct {
name string
ref Ref
wantValue string
wantErr string // substring of the ResolveError message
}{
{
name: "0600 file resolves",
ref: Ref{Name: "mpk-test", Backend: "file", Source: secretFile, Key: "API_KEY"},
wantValue: testMaterial,
},
{
name: "0400 stricter than mask passes",
ref: Ref{Name: "mpk-test", Backend: "file", Source: strictFile, Key: "API_KEY"},
wantValue: testMaterial,
},
{
name: "0644 refused insecure mode",
ref: Ref{Name: "mpk-test", Backend: "file", Source: looseFile, Key: "API_KEY"},
wantErr: "insecure_source_mode",
},
{
name: "0640 refused under default 0600 mask",
ref: Ref{Name: "mpk-test", Backend: "file", Source: groupFile, Key: "API_KEY"},
wantErr: "insecure_source_mode",
},
{
name: "0640 passes when mask says 0640",
ref: Ref{Name: "mpk-test", Backend: "file", Source: groupFile, Key: "API_KEY", Mode: "0640"},
wantValue: testMaterial,
},
{
name: "missing file",
ref: Ref{Name: "mpk-test", Backend: "file", Source: filepath.Join(dir, "nope.env"), Key: "API_KEY"},
wantErr: "unreadable_source",
},
{
name: "directory as source",
ref: Ref{Name: "mpk-test", Backend: "file", Source: dir, Key: "API_KEY"},
wantErr: "unreadable_source",
},
{
name: "missing key",
ref: Ref{Name: "mpk-test", Backend: "file", Source: secretFile, Key: "NOT_THERE"},
wantErr: "missing_key",
},
{
name: "empty value",
ref: Ref{Name: "mpk-test", Backend: "file", Source: emptyFile, Key: "API_KEY"},
wantErr: "empty_value",
},
{
name: "malformed file",
ref: Ref{Name: "mpk-test", Backend: "file", Source: brokenFile, Key: "API_KEY"},
wantErr: "malformed_source",
},
{
name: "mode mask fails closed on junk",
ref: Ref{Name: "mpk-test", Backend: "file", Source: secretFile, Key: "API_KEY", Mode: "junk"},
wantErr: "insecure_source_mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := NewFile()
got, err := f.Resolve(context.Background(), tt.ref)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("want error %q, got value", tt.wantErr)
}
msg := err.Error()
if !strings.Contains(msg, tt.wantErr) {
t.Fatalf("error %q does not contain %q", msg, tt.wantErr)
}
assertRedactedError(t, msg, tt.ref.Name, "file", testMaterial)
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.wantValue {
t.Fatalf("got %q, want %q", got, tt.wantValue)
}
})
}
}
// TestFileHomeExpansion covers ~/ expansion (generic home indirection,
// not an org-specific path).
func TestFileHomeExpansion(t *testing.T) {
t.Setenv("HOME", t.TempDir())
dir, _ := os.LookupEnv("HOME")
path := writeEnvFile(t, dir, "creds.env", "API_KEY="+testMaterial+"\n", 0o600)
f := NewFile()
got, err := f.Resolve(context.Background(), Ref{Name: "mpk-home", Backend: "file", Source: "~/creds.env", Key: "API_KEY"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != testMaterial {
t.Fatalf("got %q, want the material", got)
}
_ = path
}
// assertRedactedError enforces the house rule: resolution failures name
// the ref and the backend, NEVER material.
func assertRedactedError(t *testing.T, msg, refName, backendName, material string) {
t.Helper()
if !strings.Contains(msg, refName) || !strings.Contains(msg, backendName) {
t.Fatalf("error %q must name ref %q and backend %q", msg, refName, backendName)
}
if strings.Contains(msg, material) {
t.Fatalf("ERROR LEAKS MATERIAL: %q", msg)
}
}