feat(cli): vault bw engine env/arg builders + unlock

This commit is contained in:
Viktor Barzin 2026-06-24 10:16:19 +00:00
parent cd44ca5921
commit 06f4b87af1
2 changed files with 108 additions and 0 deletions

View file

@ -88,6 +88,62 @@ func loadCreds(run cmdRunner, user string) (vwCreds, error) {
var vaultCurrentUser = func() string { return os.Getenv("USER") }
var vaultCurrentUID = func() string { return fmt.Sprintf("%d", os.Getuid()) }
// bwBaseEnv is the minimal non-secret environment bw/node need. We deliberately
// do NOT inherit the full parent env (keeps stray secrets out of the child).
func bwBaseEnv(appdata string) []string {
path := os.Getenv("PATH")
if path == "" {
path = "/usr/local/bin:/usr/bin:/bin"
}
return []string{
"PATH=" + path,
"HOME=" + os.Getenv("HOME"),
"BITWARDENCLI_APPDATA_DIR=" + appdata,
"BW_NOINTERACTION=true",
}
}
// bwSecretEnv adds the secret-bearing vars. session may be "" (pre-unlock).
func bwSecretEnv(appdata string, c vwCreds, session string) []string {
env := bwBaseEnv(appdata)
env = append(env,
"BW_CLIENTID="+c.ClientID,
"BW_CLIENTSECRET="+c.ClientSecret,
"BW_PASSWORD="+c.MasterPassword,
)
if session != "" {
env = append(env, "BW_SESSION="+session)
}
return env
}
func bwLoginArgs() []string { return []string{"login", "--apikey"} }
func bwUnlockArgs() []string { return []string{"unlock", "--passwordenv", "BW_PASSWORD", "--raw"} }
func bwGetArgs(field, name string) []string { return []string{"get", field, name} }
func bwStatusArgs() []string { return []string{"status"} }
func bwListArgs(search string) []string {
a := []string{"list", "items"}
if search != "" {
a = append(a, "--search", search)
}
return a
}
// bwUnlock runs `bw unlock` and returns the raw session key.
func bwUnlock(run cmdRunner, env []string) (string, error) {
out, err := run("bw", bwUnlockArgs(), env)
if err != nil {
return "", fmt.Errorf("bw unlock failed (wrong master password? run `homelab vault setup`): %w", err)
}
return out, nil
}
// bwGet fetches one field of one item; session must be present in env.
func bwGet(run cmdRunner, env []string, field, name string) (string, error) {
return run("bw", bwGetArgs(field, name), env)
}
func vaultSetup(args []string) error { return fmt.Errorf("not implemented") }
func vaultStatus(args []string) error { return fmt.Errorf("not implemented") }
func vaultList(args []string) error { return fmt.Errorf("not implemented") }

View file

@ -88,3 +88,55 @@ func TestLoadCredsUnconfigured(t *testing.T) {
t.Fatalf("want 'not configured' error, got %v", err)
}
}
func TestBwEnvCarriesSecretsNotArgv(t *testing.T) {
c := vwCreds{ClientID: "user.abc", ClientSecret: "sek", MasterPassword: "hunter2"}
env := bwSecretEnv("/run/user/1001/homelab-bw", c, "SESSIONKEY")
joined := strings.Join(env, "\n")
for _, want := range []string{
"BW_CLIENTID=user.abc", "BW_CLIENTSECRET=sek", "BW_PASSWORD=hunter2",
"BW_SESSION=SESSIONKEY", "BITWARDENCLI_APPDATA_DIR=/run/user/1001/homelab-bw",
} {
if !strings.Contains(joined, want) {
t.Errorf("bwSecretEnv missing %q", want)
}
}
if strings.Contains(joined, "PATH=") == false {
t.Error("bwSecretEnv must keep a PATH so node/bw resolve")
}
}
func TestBwGetArgsHasNoSessionInArgv(t *testing.T) {
argv := bwGetArgs("password", "github")
for _, a := range argv {
if strings.Contains(a, "SESSION") || a == "--session" {
t.Fatalf("session must travel via env, not argv: %v", argv)
}
}
if !reflect.DeepEqual(argv, []string{"get", "password", "github"}) {
t.Fatalf("bwGetArgs = %v", argv)
}
}
func TestBwListArgs(t *testing.T) {
if got := bwListArgs(""); !reflect.DeepEqual(got, []string{"list", "items"}) {
t.Fatalf("bwListArgs('') = %v", got)
}
if got := bwListArgs("git"); !reflect.DeepEqual(got, []string{"list", "items", "--search", "git"}) {
t.Fatalf("bwListArgs('git') = %v", got)
}
}
func TestBwUnlockReturnsSession(t *testing.T) {
f := &fakeRunner{out: map[string]string{"bw unlock": "THE-SESSION-KEY"}}
env := bwSecretEnv("/run/user/1001/homelab-bw", vwCreds{MasterPassword: "pw"}, "")
sess, err := bwUnlock(f.run, env)
if err != nil || sess != "THE-SESSION-KEY" {
t.Fatalf("bwUnlock = %q, %v", sess, err)
}
// argv must use --passwordenv + --raw, never the password literal
last := f.calls[len(f.calls)-1]
if strings.Join(last, " ") != "bw unlock --passwordenv BW_PASSWORD --raw" {
t.Fatalf("unlock argv = %v", last)
}
}