feat: objects in vault are stored as a single secret

Co-authored-by: Dmitry Fedotov <dmitry@uint32.ru>
Co-committed-by: Dmitry Fedotov <dmitry@uint32.ru>
This commit is contained in:
2025-08-10 13:02:38 +03:00
committed by dmitry
parent 32ac8612f1
commit 3757a43318
5 changed files with 94 additions and 40 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
internal/vault/test.sh

View File

@@ -117,7 +117,7 @@ func validateKey(key string) error {
func getPrefixPath(key string) string { func getPrefixPath(key string) string {
r := []rune(key) r := []rune(key)
out := []rune{r[0], '/', r[1]} out := []rune{r[0], os.PathSeparator, r[1]}
return string(out) return string(out)
} }

View File

@@ -4,9 +4,13 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"path"
"strings"
"github.com/hashicorp/vault/api"
"code.uint32.ru/tiny/storage/internal/errinternal" "code.uint32.ru/tiny/storage/internal/errinternal"
"github.com/hashicorp/vault/api"
) )
var ( var (
@@ -14,25 +18,33 @@ var (
) )
type Storage struct { type Storage struct {
kv *api.KVv1 key string
kv *api.KVv1
// TODO: kv2: *api.KVv2 // TODO: kv2: *api.KVv2
} }
// New returns Storage writing to the specified vault path. // New returns Storage writing to the specified vault path.
// Object will be base64 encoded and written to path/key. // Objects will be base64 encoded and written to path as a single
func New(c *api.Client, path string) *Storage { // secret.
return &Storage{kv: c.KVv1(path)} func New(c *api.Client, secretpath string) *Storage {
p, k := path.Split(secretpath)
p = strings.Trim(p, "/")
k = strings.Trim(k, "/")
return &Storage{kv: c.KVv1(p), key: k}
} }
func (s *Storage) Save(key string, data []byte) error { func (s *Storage) Save(key string, data []byte) error {
str := base64.StdEncoding.EncodeToString(data) datamap, err := s.getData()
m := map[string]any{ if err != nil {
"data": map[string]string{ return err
"payload": str,
},
} }
if err := s.kv.Put(context.Background(), "testkey", m); err != nil { str := base64.StdEncoding.EncodeToString(data)
datamap[key] = str
if err := s.putData(datamap); err != nil {
return err return err
} }
@@ -40,9 +52,54 @@ func (s *Storage) Save(key string, data []byte) error {
} }
func (s *Storage) Load(key string) ([]byte, error) { func (s *Storage) Load(key string) ([]byte, error) {
m, err := s.kv.Get(context.Background(), key) data, err := s.getData()
if err != nil {
return nil, err
}
payload, ok := data[key]
if !ok {
return nil, errors.Join(ErrNotFound, errors.New("key not found in stored secret"))
}
str, ok := payload.(string)
if !ok {
// returning ErrNotFound because this will fail again on retry
return nil, errors.Join(ErrNotFound, errors.New("could not convert payload to string"))
}
b := []byte{}
b, err = base64.StdEncoding.AppendDecode(b, []byte(str))
if err != nil {
return nil, errors.Join(ErrNotFound, fmt.Errorf("could not base64 decode value: %w", err))
}
return b, nil
}
func (s *Storage) putData(data map[string]any) error {
m := map[string]any{
"data": data,
}
if err := s.kv.Put(context.Background(), s.key, m); err != nil {
return err
}
return nil
}
func (s *Storage) getData() (map[string]any, error) {
m, err := s.kv.Get(context.Background(), s.key)
if err != nil && errors.Is(err, api.ErrSecretNotFound) { if err != nil && errors.Is(err, api.ErrSecretNotFound) {
return nil, errors.Join(ErrNotFound, err) // will create secret with no payload
m := make(map[string]any)
if err := s.putData(m); err != nil {
return nil, err
}
return m, nil
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
@@ -52,33 +109,27 @@ func (s *Storage) Load(key string) ([]byte, error) {
return nil, errors.New("no data found") return nil, errors.New("no data found")
} }
payloadmap, ok := data.(map[string]any) datamap, ok := data.(map[string]any)
if !ok { if !ok {
return nil, errors.New("no payload map") return nil, errors.New("no payload map")
} }
rawb, ok := payloadmap["payload"] return datamap, nil
if !ok {
return nil, errors.New("no payload bytes")
}
str, ok := rawb.(string)
if !ok {
return nil, errors.New("could not convert payload to bytes")
}
b := []byte{}
b, err = base64.StdEncoding.AppendDecode(b, []byte(str))
if err != nil {
return nil, err
}
return b, nil
} }
func (s *Storage) Delete(key string) error { func (s *Storage) Delete(key string) error {
if err := s.kv.Delete(context.Background(), key); err != nil { data, err := s.getData()
if err != nil {
return err
}
if _, ok := data[key]; !ok {
return nil
}
delete(data, key)
if err := s.putData(data); err != nil {
return err return err
} }

View File

@@ -38,7 +38,7 @@ func TestVaultStorage(t *testing.T) {
data := []byte("this is a test") data := []byte("this is a test")
if err := st.Save(testkey, data); err != nil { if err := st.Save(testkey, data); err != nil {
t.Error(err) t.Fatalf("error saving data: %v", err)
} }
b, err := st.Load(testkey) b, err := st.Load(testkey)

View File

@@ -53,11 +53,13 @@ func NewFS(path string) (Storage, error) {
} }
// NewVault uses provided Vault client to store objects. // NewVault uses provided Vault client to store objects.
// The provided path is used as base path for // All objects are stored as a single secret, a JSON object
// keys. Objects saved to Storage will be put at // where key are keys and values are base64 encoded bytes of
// /path/key as new secrets. // saved object.
// Bytes passed to storage will be base64 encoded and saved // If secret specified by path does not exist it will be created
// in Vault as string. // on first call to Storage methods.
// Note that a secret in vault gets updated (a new version of secret is created)
// on every save/delete operation.
func NewVault(client *api.Client, path string) Storage { func NewVault(client *api.Client, path string) Storage {
return vault.New(client, path) return vault.New(client, path)
} }