From 7b507ab4a150024b9db5e7eb6c195a602135ebd8 Mon Sep 17 00:00:00 2001 From: Arvid Bjurklint Date: Thu, 15 Aug 2024 21:29:12 +0200 Subject: [PATCH] Move vault to its own package --- internal/vault/client.go | 107 ++++++++++++++++++++++++++++++++ main.go | 128 +++++---------------------------------- main_test.go | 12 ++-- 3 files changed, 130 insertions(+), 117 deletions(-) create mode 100644 internal/vault/client.go diff --git a/internal/vault/client.go b/internal/vault/client.go new file mode 100644 index 0000000..e942fd1 --- /dev/null +++ b/internal/vault/client.go @@ -0,0 +1,107 @@ +package vault + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strings" +) + +type Client struct { + Addr string + Token string + Mount string +} + +type DirEnt struct { + IsDir bool + Name string +} + +func (v Client) ListDir(name string) ([]DirEnt, error) { + url := fmt.Sprintf("%s/v1/%s/metadata%s?list=true", v.Addr, v.Mount, name) + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return []DirEnt{}, fmt.Errorf("Failed to create request: %s", err) + } + request.Header.Set("X-Vault-Token", v.Token) + request.Header.Set("Accept", "application/json") + response, err := http.DefaultClient.Do(request) + if err != nil { + return []DirEnt{}, fmt.Errorf("Failed to perform request: %s", err) + } + if response.StatusCode == 403 { + slog.Info("Forbidden to list dir", "dir", name, "url", url) + return []DirEnt{}, nil + } else if response.StatusCode != 200 { + return []DirEnt{}, fmt.Errorf("Got %s on url %s", response.Status, url) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return []DirEnt{}, fmt.Errorf("Failed to read response body: %s", err) + } + listResponse := struct { + Data struct { + Keys []string + } + }{} + if err := json.Unmarshal(body, &listResponse); err != nil { + return []DirEnt{}, fmt.Errorf("Failed to parse response body %s: %s", string(body), err) + } + entries := []DirEnt{} + for _, key := range listResponse.Data.Keys { + e := DirEnt{Name: key} + if strings.HasSuffix(key, "/") { + e.IsDir = true + } + entries = append(entries, e) + } + return entries, nil +} + +type Secret struct { + Data struct { + Data map[string]interface{} `json:"data"` + Metadata map[string]interface{} `json:"metadata"` + } `json:"data"` +} + +var cachedSecrets = make(map[string]Secret) + +func (v Client) GetSecret(name string) Secret { + if secret, found := cachedSecrets[name]; found { + return secret + } + url := fmt.Sprintf("%s/v1/%s/data%s", v.Addr, v.Mount, name) + request, err := http.NewRequest("GET", url, nil) + if err != nil { + panic(fmt.Errorf("Failed to create request: %s", err)) + } + request.Header.Set("X-Vault-Token", v.Token) + request.Header.Set("Accept", "application/json") + response, err := http.DefaultClient.Do(request) + if err != nil { + panic(fmt.Errorf("Failed to perform request: %s", err)) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + var secret Secret + if err := json.Unmarshal(body, &secret); err != nil { + panic(fmt.Errorf("Failed to unmarshal response body %s: %s", string(body), err.Error())) + } + // 404 can mean that the secret has been deleted, but it will still + // be listed. Supposedly all status codes above 400 return an + // error body. This is not true in this case. I guess we can look + // at the body and see if it has errors, if not the response is + // still valid and we can show the data. + // https://developer.hashicorp.com/vault/api-docs#error-response + isErrorForRealForReal := secret.Data.Data == nil && secret.Data.Metadata == nil + if response.StatusCode != 200 && isErrorForRealForReal { + panic(fmt.Errorf("Got %s on url %s", response.Status, url)) + } + cachedSecrets[name] = secret + return secret +} diff --git a/main.go b/main.go index 2f06e2b..8868794 100644 --- a/main.go +++ b/main.go @@ -6,13 +6,14 @@ import ( "io" "log" "log/slog" - "net/http" "os" "reflect" "slices" "strings" "sync" + "github.com/slarwise/pole3/internal/vault" + "github.com/gdamore/tcell/v2" ) @@ -33,7 +34,7 @@ type Ui struct { Screen tcell.Screen Keys []string FilteredKeys []string - Secret Secret + Secret vault.Secret Prompt string ViewStart int ViewEnd int @@ -41,14 +42,14 @@ type Ui struct { Width int Height int Result []byte - Vault VaultClient + Vault vault.Client } const SCROLL_OFF = 4 func main() { log.SetFlags(0) // Disable the timestamp - vault := VaultClient{ + vaultClient := vault.Client{ Addr: mustGetEnv("VAULT_ADDR"), Token: mustGetEnv("VAULT_TOKEN"), Mount: mustGetEnv("VAULT_MOUNT"), @@ -73,7 +74,7 @@ func main() { screen.Clear() state := Ui{ Screen: screen, - Vault: vault, + Vault: vaultClient, } quit := func() { // You have to catch panics in a defer, clean up, and @@ -92,7 +93,7 @@ func main() { drawPrompt(state) drawLoadingScreen(state) screen.Show() - state.Keys = getKeys(vault, DirEnt{IsDir: true, Name: "/"}) + state.Keys = getKeys(vaultClient, vault.DirEnt{IsDir: true, Name: "/"}) newKeysView(&state) for { ev := screen.PollEvent() @@ -147,7 +148,7 @@ func main() { } } -func getKeys(vault VaultClient, entrypoint DirEnt) []string { +func getKeys(vault vault.Client, entrypoint vault.DirEnt) []string { recv := make(chan string) go func() { recurse(recv, vault, entrypoint) @@ -160,24 +161,19 @@ func getKeys(vault VaultClient, entrypoint DirEnt) []string { return keys } -type DirEnt struct { - IsDir bool - Name string -} - -func recurse(recv chan string, vault VaultClient, entry DirEnt) { +func recurse(recv chan string, vaultClient vault.Client, entry vault.DirEnt) { if !entry.IsDir { recv <- entry.Name return } - relativeEntries, err := vault.listDir(entry.Name) + relativeEntries, err := vaultClient.ListDir(entry.Name) if err != nil { slog.Error("Failed to list directory", "directory", entry.Name, "err", err.Error()) return } - entries := []DirEnt{} + entries := []vault.DirEnt{} for _, sub := range relativeEntries { - entries = append(entries, DirEnt{ + entries = append(entries, vault.DirEnt{ IsDir: sub.IsDir, Name: entry.Name + sub.Name, }) @@ -185,106 +181,14 @@ func recurse(recv chan string, vault VaultClient, entry DirEnt) { var wg sync.WaitGroup for _, e := range entries { wg.Add(1) - go func(entry DirEnt) { + go func(entry vault.DirEnt) { defer wg.Done() - recurse(recv, vault, e) + recurse(recv, vaultClient, e) }(e) } wg.Wait() } -type VaultClient struct { - Addr string - Token string - Mount string -} - -func (v VaultClient) listDir(name string) ([]DirEnt, error) { - url := fmt.Sprintf("%s/v1/%s/metadata%s?list=true", v.Addr, v.Mount, name) - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return []DirEnt{}, fmt.Errorf("Failed to create request: %s", err) - } - request.Header.Set("X-Vault-Token", v.Token) - request.Header.Set("Accept", "application/json") - response, err := http.DefaultClient.Do(request) - if err != nil { - return []DirEnt{}, fmt.Errorf("Failed to perform request: %s", err) - } - if response.StatusCode == 403 { - slog.Info("Forbidden to list dir", "dir", name, "url", url) - return []DirEnt{}, nil - } else if response.StatusCode != 200 { - return []DirEnt{}, fmt.Errorf("Got %s on url %s", response.Status, url) - } - defer response.Body.Close() - body, err := io.ReadAll(response.Body) - if err != nil { - return []DirEnt{}, fmt.Errorf("Failed to read response body: %s", err) - } - listResponse := struct { - Data struct { - Keys []string - } - }{} - if err := json.Unmarshal(body, &listResponse); err != nil { - return []DirEnt{}, fmt.Errorf("Failed to parse response body %s: %s", string(body), err) - } - entries := []DirEnt{} - for _, key := range listResponse.Data.Keys { - e := DirEnt{Name: key} - if strings.HasSuffix(key, "/") { - e.IsDir = true - } - entries = append(entries, e) - } - return entries, nil -} - -type Secret struct { - Data struct { - Data map[string]interface{} `json:"data"` - Metadata map[string]interface{} `json:"metadata"` - } `json:"data"` -} - -var cachedSecrets = make(map[string]Secret) - -func (v VaultClient) getSecret(name string) Secret { - if secret, found := cachedSecrets[name]; found { - return secret - } - url := fmt.Sprintf("%s/v1/%s/data%s", v.Addr, v.Mount, name) - request, err := http.NewRequest("GET", url, nil) - if err != nil { - panic(fmt.Errorf("Failed to create request: %s", err)) - } - request.Header.Set("X-Vault-Token", v.Token) - request.Header.Set("Accept", "application/json") - response, err := http.DefaultClient.Do(request) - if err != nil { - panic(fmt.Errorf("Failed to perform request: %s", err)) - } - defer response.Body.Close() - body, err := io.ReadAll(response.Body) - var secret Secret - if err := json.Unmarshal(body, &secret); err != nil { - panic(fmt.Errorf("Failed to unmarshal response body %s: %s", string(body), err.Error())) - } - // 404 can mean that the secret has been deleted, but it will still - // be listed. Supposedly all status codes above 400 return an - // error body. This is not true in this case. I guess we can look - // at the body and see if it has errors, if not the response is - // still valid and we can show the data. - // https://developer.hashicorp.com/vault/api-docs#error-response - isErrorForRealForReal := secret.Data.Data == nil && secret.Data.Metadata == nil - if response.StatusCode != 200 && isErrorForRealForReal { - panic(fmt.Errorf("Got %s on url %s", response.Status, url)) - } - cachedSecrets[name] = secret - return secret -} - func drawLine(s tcell.Screen, x, y int, style tcell.Style, text string) { for _, r := range []rune(text) { s.SetContent(x, y, r, nil, style) @@ -461,9 +365,9 @@ func newKeysView(s *Ui) { func setSecret(s *Ui) { if len(s.FilteredKeys) > 0 { - s.Secret = s.Vault.getSecret(s.FilteredKeys[s.ViewStart+s.Cursor]) + s.Secret = s.Vault.GetSecret(s.FilteredKeys[s.ViewStart+s.Cursor]) } else { - s.Secret = Secret{} + s.Secret = vault.Secret{} } } diff --git a/main_test.go b/main_test.go index a52814e..f4047b1 100644 --- a/main_test.go +++ b/main_test.go @@ -7,6 +7,8 @@ import ( "slices" "testing" "time" + + "github.com/slarwise/pole3/internal/vault" ) const ( @@ -33,16 +35,16 @@ func TestGetKeys(t *testing.T) { if err := populate(vaultAddr, token, secrets); err != nil { t.Fatalf("Failed to populate vault with secrets: %s", err.Error()) } - vault := VaultClient{ + vaultClient := vault.Client{ Addr: vaultAddr, Token: token, Mount: "secret", } - entrypoint := DirEnt{ + entrypoint := vault.DirEnt{ IsDir: true, Name: "/", } - keys := getKeys(vault, entrypoint) + keys := getKeys(vaultClient, entrypoint) if len(keys) != len(secrets) { t.Fatalf("Expected %d keys, got %d", len(secrets), len(keys)) } @@ -70,12 +72,12 @@ func TestGetSecret(t *testing.T) { if err := populate(vaultAddr, token, secrets); err != nil { t.Fatalf("Failed to populate vault with secrets: %s", err.Error()) } - vault := VaultClient{ + vaultClient := vault.Client{ Addr: vaultAddr, Token: token, Mount: "secret", } - secret, err := vault.getSecret("/bar/baz") + secret := vaultClient.GetSecret("/bar/baz") if err != nil { t.Fatalf("Got unexpected error: %s", err) }