/
client.go
226 lines (189 loc) · 6.35 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package vaku
import (
"context"
"errors"
"fmt"
"strings"
vault "github.com/hashicorp/vault/api"
)
var (
// ErrNumWorkers when workers is not a supported number.
ErrNumWorkers = errors.New("invalid workers")
// ErrApplyOptions when options fails to apply.
ErrApplyOptions = errors.New("applying options")
)
const (
defaultWorkers = 10
)
// logical is functions from vault.Logical() used by Vaku. Helps with testing.
type logical interface {
Delete(path string) (*vault.Secret, error)
List(path string) (*vault.Secret, error)
Read(path string) (*vault.Secret, error)
Write(path string, data map[string]any) (*vault.Secret, error)
}
// Client has all Vaku functions and wraps Vault API clients.
type Client struct {
// vc is the vault client.
vc *vault.Client
// vl wraps vc.Logical() for easy testing.
vl logical
// dc is a recursive Client for operations with a source and destination.
dc *Client
// workers is the max number of concurrent operations against vault.
workers int
// absolutePath if the absolute path is desired instead of the relative path.
absolutePath bool
// mountProvider provides a list of all mounts.
mountProvider mountProvider
}
// ClientInterface exports the interface for the full Vaku client.
type ClientInterface interface {
PathList(string) ([]string, error)
PathRead(string) (map[string]any, error)
PathWrite(string, map[string]any) error
PathDelete(string) error
PathDeleteMeta(string) error
PathDestroy(string, []int) error
PathUpdate(string, map[string]any) error
PathSearch(string, string) (bool, error)
PathCopy(string, string) error
PathMove(string, string) error
FolderList(context.Context, string) ([]string, error)
FolderListChan(context.Context, string) (<-chan string, <-chan error)
FolderRead(context.Context, string) (map[string]map[string]any, error)
FolderReadChan(context.Context, string) (<-chan map[string]map[string]any, <-chan error)
FolderWrite(context.Context, map[string]map[string]any) error
FolderDelete(context.Context, string) error
FolderDeleteMeta(context.Context, string) error
FolderDestroy(context.Context, string, []int) error
FolderSearch(context.Context, string, string) ([]string, error)
FolderCopy(context.Context, string, string) error
FolderMove(context.Context, string, string) error
}
// Verify Client compliance with the interface.
var _ ClientInterface = (*Client)(nil)
// Option configures a Client.
type Option interface {
apply(c *Client) error
}
// WithVaultClient sets the Vault client to be used.
func WithVaultClient(c *vault.Client) Option {
return withVaultClient{c}
}
// WithVaultSrcClient is an alias for WithVaultClient.
func WithVaultSrcClient(c *vault.Client) Option {
return withVaultClient{c}
}
type withVaultClient struct {
client *vault.Client
}
func (o withVaultClient) apply(c *Client) error {
c.vc = o.client
c.vl = o.client.Logical()
return nil
}
// WithVaultDstClient sets a separate Vault client to be used only on operations that have a source
// and destination (copy, move, etc...). If unset the source client will be used.
func WithVaultDstClient(c *vault.Client) Option {
return withVaultDstClient{c}
}
type withVaultDstClient struct {
client *vault.Client
}
func (o withVaultDstClient) apply(c *Client) error {
// By default the dest client is just a pointer to the source client. So make a copy of the
// source, assign new vault client to the copy, and assign copy back to the dest.
newDstClient := *c.dc
newDstClient.vc = o.client
newDstClient.vl = o.client.Logical()
c.dc = &newDstClient
return nil
}
// WithWorkers sets the maximum number of goroutines that access Vault at any given time. Does not
// cap the number of goroutines overall. Default value is 10. A stable and well-operated Vault
// server should be able to handle 100 or more without issue. Use with caution and tune specifically
// to your environment and storage backend.
func WithWorkers(n int) Option {
return withWorkers(n)
}
type withWorkers int
func (o withWorkers) apply(c *Client) error {
if o < 1 {
return newWrapErr(fmt.Sprintf("workers must 1 or greater: %d", o), ErrNumWorkers, nil)
}
c.workers = int(o)
c.dc.workers = int(o)
return nil
}
// WithAbsolutePath sets the output format for all returned paths. Default path output is a relative
// path, trimmed up to the path input. Pass WithAbsolutePath(true) to set path output to the entire
// path. Example: List(secret/foo) -> "bar" OR "secret/foo/bar".
func WithAbsolutePath(b bool) Option {
return withAbsolutePath(b)
}
type withAbsolutePath bool
func (o withAbsolutePath) apply(c *Client) error {
c.absolutePath = bool(o)
c.dc.absolutePath = bool(o)
return nil
}
// WithMountProvider makes it possible to inject a custom method for listing mounts.
// The default method uses the sys/mounts endpoint. This requires a level of privilege that
// not all users may have.
func WithMountProvider(p mountProvider) Option {
return withMountProvider{provider: p}
}
type withMountProvider struct {
provider mountProvider
}
func (o withMountProvider) apply(c *Client) error {
c.mountProvider = o.provider
return nil
}
// NewClient returns a new Vaku Client based on the Vault API config.
func NewClient(opts ...Option) (*Client, error) {
// set defaults
client := &Client{
workers: defaultWorkers,
}
client.dc = client
client.mountProvider = defaultMountProvider{
client: client,
}
// apply options
for _, opt := range opts {
err := opt.apply(client)
if err != nil {
return nil, newWrapErr("", ErrApplyOptions, err)
}
}
return client, nil
}
// swapPaths replaces source paths in data with dest paths for copy/move after FolderRead.
func (c *Client) swapPaths(data map[string]map[string]any, src, dst string) {
if c.absolutePath {
TrimPrefixMap(data, src)
}
EnsurePrefixMap(data, dst)
}
// inputPath returns a path ready for input into a read/write/list/delete.
func (c *Client) inputPath(path, root string) string {
if c.absolutePath {
return path
}
return AddPrefix(path, root)
}
// outputPath returns a path for the user, given their formatting preferences.
func (c *Client) outputPath(path, root string) string {
if c.absolutePath {
return EnsurePrefix(path, root)
}
return PathJoin(strings.TrimPrefix(path, root))
}
// outputPaths prepares a list of paths for the user, given their formatting preferences.
func (c *Client) outputPaths(paths []string, root string) {
if c.absolutePath {
AddPrefixList(paths, root)
}
}