From 06f4b87af10d4b5c9846e408944faaa46dbd6eb8 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Wed, 24 Jun 2026 10:16:19 +0000 Subject: [PATCH] feat(cli): vault bw engine env/arg builders + unlock --- cli/cmd_vault.go | 56 +++++++++++++++++++++++++++++++++++++++++++ cli/cmd_vault_test.go | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/cli/cmd_vault.go b/cli/cmd_vault.go index 70aa5cd3..4ee2ad47 100644 --- a/cli/cmd_vault.go +++ b/cli/cmd_vault.go @@ -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") } diff --git a/cli/cmd_vault_test.go b/cli/cmd_vault_test.go index 76448484..52a91a47 100644 --- a/cli/cmd_vault_test.go +++ b/cli/cmd_vault_test.go @@ -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) + } +}