Skip to content

Commit 155114b

Browse files
TediferousTed Dorfeuillemterwill
authored
feat(secrets): support for csi directories (#565)
Co-authored-by: Ted Dorfeuille <[email protected]> Co-authored-by: Matt Terwilliger <[email protected]>
1 parent 1f8e297 commit 155114b

File tree

3 files changed

+165
-18
lines changed

3 files changed

+165
-18
lines changed

secrets/secrets.go

+59-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"io/fs"
78

89
"github.com/reddit/baseplate.go/errorsbp"
910
)
@@ -28,6 +29,11 @@ func (s Secret) IsEmpty() bool {
2829
return len(s) == 0
2930
}
3031

32+
// CSIFile represents the raw parsed object of a file made by the Vault CSI provider
33+
type CSIFile struct {
34+
Secret GenericSecret `json:"data"`
35+
}
36+
3137
// Secrets allows to access secrets based on their different type.
3238
type Secrets struct {
3339
simpleSecrets map[string]SimpleSecret
@@ -258,16 +264,32 @@ func NewSecrets(r io.Reader) (*Secrets, error) {
258264
return nil, err
259265
}
260266

261-
err = secretsDocument.Validate()
267+
return secretsValidate(secretsDocument)
268+
}
269+
270+
// FromDir parses a directory and returns its secrets
271+
func FromDir(dir fs.FS) (*Secrets, error) {
272+
secretsDocument := Document{
273+
Secrets: make(map[string]GenericSecret),
274+
}
275+
secretsDocument, err := walkCSIDirectory(dir)
262276
if err != nil {
263277
return nil, err
264278
}
279+
return secretsValidate(secretsDocument)
280+
281+
}
282+
283+
func secretsValidate(secretsDocument Document) (*Secrets, error) {
265284
secrets := &Secrets{
266285
simpleSecrets: make(map[string]SimpleSecret),
267286
versionedSecrets: make(map[string]VersionedSecret),
268287
credentialSecrets: make(map[string]CredentialSecret),
269288
vault: secretsDocument.Vault,
270289
}
290+
if err := secretsDocument.Validate(); err != nil {
291+
return nil, err
292+
}
271293
for key, secret := range secretsDocument.Secrets {
272294
switch secret.Type {
273295
case "simple":
@@ -298,3 +320,39 @@ func NewSecrets(r io.Reader) (*Secrets, error) {
298320
}
299321
return secrets, nil
300322
}
323+
324+
// walkCSIDirectory parses a directory for vault secrets and merges them into one object
325+
func walkCSIDirectory(dir fs.FS) (Document, error) {
326+
secretsDocument := Document{
327+
Secrets: make(map[string]GenericSecret),
328+
}
329+
err := fs.WalkDir(
330+
dir,
331+
".",
332+
func(path string, d fs.DirEntry, err error) error {
333+
if err != nil {
334+
return err
335+
}
336+
if d.IsDir() {
337+
return nil
338+
}
339+
file, err := dir.Open(path)
340+
if err != nil {
341+
return err
342+
}
343+
defer file.Close()
344+
345+
var secretFile CSIFile
346+
err = json.NewDecoder(file).Decode(&secretFile)
347+
if err != nil {
348+
return fmt.Errorf("decoding %q: %w", path, err)
349+
}
350+
secretsDocument.Secrets[path] = secretFile.Secret
351+
return nil
352+
},
353+
)
354+
if err != nil {
355+
return Document{}, fmt.Errorf("Error during walkCSIDirectory: %w", err)
356+
}
357+
return secretsDocument, nil
358+
}

secrets/store.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package secrets
33
import (
44
"context"
55
"io"
6+
"io/fs"
7+
"os"
68

79
"github.com/reddit/baseplate.go/filewatcher"
810
"github.com/reddit/baseplate.go/log"
@@ -42,12 +44,21 @@ func NewStore(ctx context.Context, path string, logger log.Wrapper, middlewares
4244
secretHandlerFunc: nopSecretHandlerFunc,
4345
}
4446
store.secretHandler(middlewares...)
47+
fileInfo, err := os.Stat(path)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
parser := store.parser
53+
if fileInfo.IsDir() {
54+
parser = filewatcher.WrapDirParser(store.dirParser)
55+
}
4556

4657
result, err := filewatcher.New(
4758
ctx,
4859
filewatcher.Config{
4960
Path: path,
50-
Parser: store.parser,
61+
Parser: parser,
5162
Logger: logger,
5263
},
5364
)
@@ -59,7 +70,7 @@ func NewStore(ctx context.Context, path string, logger log.Wrapper, middlewares
5970
return store, nil
6071
}
6172

62-
func (s *Store) parser(r io.Reader) (interface{}, error) {
73+
func (s *Store) parser(r io.Reader) (any, error) {
6374
secrets, err := NewSecrets(r)
6475
if err != nil {
6576
return nil, err
@@ -70,6 +81,17 @@ func (s *Store) parser(r io.Reader) (interface{}, error) {
7081
return secrets, nil
7182
}
7283

84+
func (s *Store) dirParser(dir fs.FS) (any, error) {
85+
secrets, err := FromDir(dir)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
s.secretHandlerFunc(secrets)
91+
92+
return secrets, nil
93+
}
94+
7395
// secretHandler creates the middleware chain.
7496
func (s *Store) secretHandler(middlewares ...SecretMiddleware) {
7597
for _, m := range middlewares {

secrets/store_test.go

+82-15
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,72 @@ const specificationExample = `
3737
}
3838
}`
3939

40+
var externalAccountKey = `
41+
{
42+
"request_id": "1afc3036-2282-d483-c2d4-6d483efdf16c",
43+
"lease_id": "",
44+
"lease_duration": 2764800,
45+
"renewable": false,
46+
"data": {
47+
"type": "versioned",
48+
"current": "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=",
49+
"previous": "aHVudGVyMg=="
50+
},
51+
"warnings": null
52+
}
53+
`
54+
55+
var someAPIKey = `
56+
{
57+
"request_id": "1afc3036-2282-d483-c2d4-6d483efdf16c",
58+
"lease_id": "",
59+
"lease_duration": 2764800,
60+
"renewable": false,
61+
"data": {
62+
"type": "simple",
63+
"value": "Y2RvVXhNMVdsTXJma3BDaHRGZ0dPYkVGSg==",
64+
"encoding": "base64"
65+
},
66+
"warnings": null
67+
}
68+
`
69+
var someDatabaseCredentials = `
70+
{
71+
"request_id": "1afc3036-2282-d483-c2d4-6d483efdf16c",
72+
"lease_id": "",
73+
"lease_duration": 2764800,
74+
"renewable": false,
75+
"data": {
76+
"type": "credential",
77+
"username": "spez",
78+
"password": "hunter2"
79+
},
80+
"warnings": null
81+
}
82+
`
83+
4084
func TestGetSimpleSecret(t *testing.T) {
4185
dir := t.TempDir()
86+
dirCSI := t.TempDir()
4287
tmpFile, err := os.CreateTemp(dir, "secrets.json")
4388
if err != nil {
4489
t.Fatal(err)
4590
}
91+
if err := os.Mkdir(dirCSI+"/secret", 0777); err != nil {
92+
t.Fatal(err)
93+
}
94+
if err := os.Mkdir(dirCSI+"/secret/myservice", 0777); err != nil {
95+
t.Fatal(err)
96+
}
97+
if err := os.WriteFile(dirCSI+"/secret/myservice/external-account-key", []byte(externalAccountKey), 0777); err != nil {
98+
t.Fatal(err)
99+
}
100+
if err := os.WriteFile(dirCSI+"/secret/myservice/some-api-key", []byte(someAPIKey), 0777); err != nil {
101+
t.Fatal(err)
102+
}
103+
if err := os.WriteFile(dirCSI+"/secret/myservice/some-database-credentials", []byte(someDatabaseCredentials), 0777); err != nil {
104+
t.Fatal(err)
105+
}
46106
tmpPath := tmpFile.Name()
47107
tmpFile.Write([]byte(specificationExample))
48108
if err := tmpFile.Close(); err != nil {
@@ -71,21 +131,28 @@ func TestGetSimpleSecret(t *testing.T) {
71131
t.Run(
72132
tt.name,
73133
func(t *testing.T) {
74-
store, err := secrets.NewStore(context.Background(), tmpPath, log.TestWrapper(t))
75-
if err != nil {
76-
t.Fatal(err)
77-
}
78-
defer store.Close()
79-
80-
secret, err := store.GetSimpleSecret(tt.key)
81-
if tt.expectedError == nil && err != nil {
82-
t.Fatal(err)
83-
}
84-
if tt.expectedError != nil && err.Error() != tt.expectedError.Error() {
85-
t.Fatalf("expected error %v, actual: %v", tt.expectedError, err)
86-
}
87-
if !reflect.DeepEqual(secret, tt.expected) {
88-
t.Fatalf("expected %+v, actual: %+v", tt.expected, secret)
134+
for label, path := range map[string]string{
135+
"file": tmpPath,
136+
"dir": dirCSI,
137+
} {
138+
t.Run(label, func(t *testing.T) {
139+
store, err := secrets.NewStore(context.Background(), path, log.TestWrapper(t))
140+
if err != nil {
141+
t.Fatal(err)
142+
}
143+
t.Cleanup(func() { store.Close() })
144+
145+
secret, err := store.GetSimpleSecret(tt.key)
146+
if tt.expectedError == nil && err != nil {
147+
t.Fatal(err)
148+
}
149+
if tt.expectedError != nil && err.Error() != tt.expectedError.Error() {
150+
t.Fatalf("expected error %v, actual: %v", tt.expectedError, err)
151+
}
152+
if !reflect.DeepEqual(secret, tt.expected) {
153+
t.Fatalf("expected %+v, actual: %+v", tt.expected, secret)
154+
}
155+
})
89156
}
90157
},
91158
)

0 commit comments

Comments
 (0)