feat(cli): homelab vault setup onboarding (one-time, self-service)
This commit is contained in:
parent
e20033855d
commit
5a864cf19c
2 changed files with 76 additions and 1 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -459,7 +460,71 @@ func vaultLock(args []string) error {
|
||||||
return nil // lock/logout best-effort; never error the caller
|
return nil // lock/logout best-effort; never error the caller
|
||||||
}
|
}
|
||||||
|
|
||||||
func vaultSetup(args []string) error { return fmt.Errorf("not implemented") }
|
func vaultPutArgs(user string, c vwCreds) []string {
|
||||||
|
return []string{"kv", "patch", vwCredsPath(user),
|
||||||
|
"vaultwarden_email=" + c.Email,
|
||||||
|
"vaultwarden_master_password=" + c.MasterPassword,
|
||||||
|
"vaultwarden_client_id=" + c.ClientID,
|
||||||
|
"vaultwarden_client_secret=" + c.ClientSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptNoEcho reads one line without terminal echo (for the master password).
|
||||||
|
func promptNoEcho(prompt string) (string, error) {
|
||||||
|
fmt.Fprint(os.Stderr, prompt)
|
||||||
|
exec.Command("stty", "-echo").Run()
|
||||||
|
defer func() { exec.Command("stty", "echo").Run(); fmt.Fprintln(os.Stderr) }()
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
line, err := r.ReadString('\n')
|
||||||
|
return strings.TrimSpace(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptLine(prompt string) (string, error) {
|
||||||
|
fmt.Fprint(os.Stderr, prompt)
|
||||||
|
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
|
return strings.TrimSpace(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func vaultSetup(args []string) error {
|
||||||
|
hardenProcess()
|
||||||
|
fmt.Fprintln(os.Stderr, "One-time setup. Stored ONLY in your own Vault path; the admin never sees it.")
|
||||||
|
fmt.Fprintln(os.Stderr, "Get your API key at https://vaultwarden.viktorbarzin.me → Settings → Security → Keys → View API key.")
|
||||||
|
email, err := promptLine("Vaultwarden email: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clientID, err := promptLine("API key client_id (user.xxxx): ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clientSecret, err := promptNoEcho("API key client_secret: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
master, err := promptNoEcho("Master password: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if master == "" || clientID == "" || clientSecret == "" {
|
||||||
|
return fmt.Errorf("all fields are required")
|
||||||
|
}
|
||||||
|
c := vwCreds{Email: email, MasterPassword: master, ClientID: clientID, ClientSecret: clientSecret}
|
||||||
|
if _, err := realRunner("vault", vaultPutArgs(vaultCurrentUser(), c), nil); err != nil {
|
||||||
|
return fmt.Errorf("writing creds to your Vault path failed (scoped token present?): %w", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "Stored. Verifying unlock…")
|
||||||
|
uid := vaultCurrentUID()
|
||||||
|
unlock, err := withUserLock(uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
if _, err := openSession(realRunner, vaultCurrentUser(), uid); err != nil {
|
||||||
|
return fmt.Errorf("stored, but verification failed — double-check master password / API key: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "✓ Verified. Fetches are now AFK.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func vaultGet(args []string) error {
|
func vaultGet(args []string) error {
|
||||||
hardenProcess()
|
hardenProcess()
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,16 @@ func TestStatusSummaryUnconfigured(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVaultPutArgs(t *testing.T) {
|
||||||
|
got := vaultPutArgs("emo", vwCreds{Email: "e", MasterPassword: "m", ClientID: "ci", ClientSecret: "cs"})
|
||||||
|
want := []string{"kv", "patch", "secret/workstation/claude-users/emo",
|
||||||
|
"vaultwarden_email=e", "vaultwarden_master_password=m",
|
||||||
|
"vaultwarden_client_id=ci", "vaultwarden_client_secret=cs"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("vaultPutArgs = %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getValue is the testable core: given a runner + opts, returns the secret value.
|
// getValue is the testable core: given a runner + opts, returns the secret value.
|
||||||
func TestGetValueFlow(t *testing.T) {
|
func TestGetValueFlow(t *testing.T) {
|
||||||
f := &fakeRunner{out: map[string]string{
|
f := &fakeRunner{out: map[string]string{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue