/
vault_client.go
218 lines (183 loc) · 7.03 KB
/
vault_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
package vault
import (
"encoding/base64"
"fmt"
"net/url"
"regexp"
"github.com/jenkins-x/jx-logging/pkg/log"
"github.com/hashicorp/vault/api"
"github.com/jenkins-x/jx/v2/pkg/secreturl"
"github.com/jenkins-x/jx/v2/pkg/util"
"github.com/pkg/errors"
)
const (
yamlDataKey = "yaml"
defaultSecretEngineMountPoint = "secret"
)
var vaultURIRegex = regexp.MustCompile(`:[\s"]*vault:[-_.\w\/:]*`)
// Client is an interface for interacting with Vault
//go:generate pegomock generate github.com/jenkins-x/jx/v2/pkg/vault Client -o mocks/vault_client.go
type Client interface {
// Write writes a named secret to the vault
Write(secretName string, data map[string]interface{}) (map[string]interface{}, error)
// WriteObject writes a generic named object to the vault.
// The secret _must_ be serializable to JSON.
WriteObject(secretName string, secret interface{}) (map[string]interface{}, error)
// WriteYaml writes a yaml object to a named secret
WriteYaml(secretName string, yamlstring string) (map[string]interface{}, error)
// List lists the secrets under the specified path
List(path string) ([]string, error)
// Read reads a named secret from the vault
Read(secretName string) (map[string]interface{}, error)
// ReadObject reads a generic named object from vault.
// The secret _must_ be serializable to JSON.
ReadObject(secretName string, secret interface{}) error
// ReadYaml reads a yaml object from a named secret
ReadYaml(secretName string) (string, error)
// Config gets the config required for configuring the official Vault CLI
Config() (vaultURL url.URL, vaultToken string, err error)
// ReplaceURIs will replace any vault: URIs in a string (or whatever URL scheme the secret URL client supports
ReplaceURIs(text string) (string, error)
}
// client is a wrapper around the official Vault API
type client struct {
client *api.Client
secretEngineMountPoint string
}
// NewVaultClient creates a new Vault Client wrapping the provided api.Client. The provided secretEngineMountPoint determines the
// prefix (mount point) for the KV engine used by this client. If the empty string is specified, the string 'secret' is assumed as
// the default prefix.
func NewVaultClient(apiClient *api.Client, secretEngineMountPoint string) Client {
if secretEngineMountPoint == "" {
secretEngineMountPoint = defaultSecretEngineMountPoint
}
return &client{
client: apiClient,
secretEngineMountPoint: secretEngineMountPoint,
}
}
// Write writes a named secret to the vault with the data provided. Data can be a generic map of stuff, but at all points
// in the map, keys _must_ be strings (not bool, int or even interface{}) otherwise you'll get an error
func (v *client) Write(secretName string, data map[string]interface{}) (map[string]interface{}, error) {
payload := map[string]interface{}{
"data": data,
}
path := v.secretPath(secretName)
log.Logger().Tracef("writing secret %s", path)
secret, err := v.client.Logical().Write(path, payload)
if secret != nil {
return secret.Data, err
}
return nil, err
}
// Read reads a named secret to the vault
func (v *client) Read(secretName string) (map[string]interface{}, error) {
secret, err := v.client.Logical().Read(v.secretPath(secretName))
if err != nil {
return nil, errors.Wrapf(err, "reading secret %q from vault", secretName)
}
if secret == nil {
return nil, fmt.Errorf("no secret %q not found in vault", secretName)
}
if secret.Data != nil {
data, ok := secret.Data["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid data type for secret %q", secretName)
}
return data, nil
}
return nil, fmt.Errorf("no data found on secret %q", secretName)
}
// WriteObject writes a generic named object to the vault. The secret _must_ be serializable to JSON
func (v *client) WriteObject(secretName string, secret interface{}) (map[string]interface{}, error) {
// Convert the secret into a saveable map[string]interface{} format
m, err := util.ToMapStringInterfaceFromStruct(&secret)
if err != nil {
return nil, errors.Wrapf(err, "serializing secret %q object for saving to vault", secretName)
}
return v.Write(secretName, m)
}
// ReadObject reads a generic named object from the vault.
func (v *client) ReadObject(secretName string, secret interface{}) error {
m, err := v.Read(secretName)
if err != nil {
return errors.Wrapf(err, "reading the secret %q from vault", secretName)
}
err = util.ToStructFromMapStringInterface(m, &secret)
if err != nil {
return errors.Wrapf(err, "deserializing the secret %q from vault", secretName)
}
return nil
}
// WriteYaml writes a yaml object to a named secret
func (v *client) WriteYaml(secretName string, y string) (map[string]interface{}, error) {
data := base64.StdEncoding.EncodeToString([]byte(y))
secretMap := map[string]interface{}{
yamlDataKey: data,
}
return v.Write(secretName, secretMap)
}
// ReadYaml reads a yaml object from a named secret
func (v *client) ReadYaml(secretName string) (string, error) {
secretMap, err := v.Read(secretName)
if err != nil {
return "", errors.Wrapf(err, "reading secret %q from vault", secretName)
}
data, ok := secretMap[yamlDataKey]
if !ok {
return "", nil
}
strData, ok := data.(string)
if !ok {
return "", fmt.Errorf("data stored at secret key %s/%s is not a valid string", secretName, yamlDataKey)
}
decodedData, err := base64.StdEncoding.DecodeString(strData)
if err != nil {
return "", errors.Wrapf(err, "decoding base64 data stored at secret key %s/%s", secretName, yamlDataKey)
}
return string(decodedData), nil
}
// List lists the secrets under a given path
func (v *client) List(path string) ([]string, error) {
secrets, err := v.client.Logical().List(v.secretMetadataPath(path))
if err != nil {
return nil, err
}
secretNames := make([]string, 0)
if secrets == nil {
return secretNames, nil
}
data := secrets.Data
if data == nil {
return secretNames, nil
}
// Don't do type assertion on nil
keys := secrets.Data["keys"]
if keys == nil {
return secretNames, nil
}
for _, s := range keys.([]interface{}) {
if orig, ok := s.(string); ok {
secretNames = append(secretNames, orig)
}
}
return secretNames, nil
}
// Config returns the current vault address and api token
func (v *client) Config() (vaultURL url.URL, vaultToken string, err error) {
parsed, err := url.Parse(v.client.Address())
return *parsed, v.client.Token(), err
}
// ReplaceURIs will replace any vault: URIs in a string (or whatever URL scheme the secret URL client supports
func (v *client) ReplaceURIs(s string) (string, error) {
return secreturl.ReplaceURIs(s, v, vaultURIRegex, "vault:")
}
// secretPath generates a secret path from the secret path for storing in vault
// this just makes sure it gets stored under /secret
func (v *client) secretPath(path string) string {
return fmt.Sprintf("%s/data/%s", v.secretEngineMountPoint, path)
}
// secretMetaPath generates the secret metadata path form the secret path provided
func (v *client) secretMetadataPath(path string) string {
return fmt.Sprintf("%s/metadata/%s", v.secretEngineMountPoint, path)
}