Skip to content

Commit

Permalink
Move vault to its own package
Browse files Browse the repository at this point in the history
  • Loading branch information
slarwise committed Aug 15, 2024
1 parent 131eb43 commit 7b507ab
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 117 deletions.
107 changes: 107 additions & 0 deletions internal/vault/client.go
Original file line number Diff line number Diff line change
@@ -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
}
128 changes: 16 additions & 112 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -33,22 +34,22 @@ type Ui struct {
Screen tcell.Screen
Keys []string
FilteredKeys []string
Secret Secret
Secret vault.Secret
Prompt string
ViewStart int
ViewEnd int
Cursor int
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"),
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -160,131 +161,34 @@ 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,
})
}
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)
Expand Down Expand Up @@ -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{}
}
}

Expand Down
12 changes: 7 additions & 5 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"slices"
"testing"
"time"

"github.com/slarwise/pole3/internal/vault"
)

const (
Expand All @@ -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))
}
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 7b507ab

Please sign in to comment.