-
Notifications
You must be signed in to change notification settings - Fork 3
/
secret.go
256 lines (215 loc) · 8.17 KB
/
secret.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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
package skipper
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/lukasjarosch/skipper/secret"
"github.com/spf13/afero"
)
// secretRegex match pattern
// ?{driver:path/to/file||ifNotExistsAction:actionParam}
var secretRegex = regexp.MustCompile(`\?\{(\w+)\:([\w\/\-\.\_]+)(\|\|([\w\-\_\.\:]+))?\}`)
type Secret struct {
*SecretFile
Driver secret.Driver
DriverName string
AlternativeCall *Call
Identifier []interface{}
}
func NewSecret(secretFile *SecretFile, driver string, alternative *Call, path []interface{}) (*Secret, error) {
return &Secret{
SecretFile: secretFile,
Driver: nil,
DriverName: driver,
Identifier: path,
AlternativeCall: alternative,
}, nil
}
// SecretFileData describes the generic structure of secret files.
type SecretFileData struct {
Data string `yaml:"data"`
Type string `yaml:"type"`
Key string `yaml:"key"`
}
// NewSecretData constructs a [Data] map as it is required for secrets.
func NewSecretData(data string, driver string, key string) (*SecretFileData, error) {
if data == "" {
return nil, fmt.Errorf("secret data cannot be empty")
}
if driver == "" {
return nil, fmt.Errorf("secret file cannot have an empty type")
}
return &SecretFileData{
Data: data,
Type: driver,
Key: key,
}, nil
}
// FindSecrets will leverage the `FindValues` function of [Data] to recursively search for secrets.
// All returned values are converted to *Secret and then returned as []*Secret.
func FindOrCreateSecrets(data Data, secretFiles SecretFileList, secretPath string, fs afero.Fs) ([]*Secret, error) {
var foundValues []interface{}
err := data.FindValues(secretFindValueFunc(secretFiles), &foundValues)
if err != nil {
return nil, err
}
var foundSecrets []*Secret
for _, val := range foundValues {
// secretFindValueFunc returns []*Secret so we need to ensure that matches
vars, ok := val.([]*Secret)
if !ok {
return nil, fmt.Errorf("unexpected error during secret detection, file a bug report")
}
for _, sec := range vars {
// ensure that the driver is loaded and assigned to every secret
driver, err := secret.SecretDriverFactory(sec.DriverName)
if err != nil {
return nil, fmt.Errorf("cannot get secret driver '%s': %w", sec.DriverName, err)
}
sec.Driver = driver
// secrets which do not have a file associated are candidates for automatic creation
if sec.SecretFile.YamlFile == nil {
err = sec.attemptCreate(fs, secretPath)
if err != nil {
return nil, fmt.Errorf("failed to auto-create secret: %w", err)
}
}
}
foundSecrets = append(foundSecrets, vars...)
}
return foundSecrets, nil
}
// ReplaceSecret will replace the given secret inside Data with the actual secret value.
func ReplaceSecret(data Data, secret *Secret) error {
// sourceValue is the value where the variable is. It needs to be replaced with an actual value
sourceValue, err := data.GetPath(secret.Identifier...)
if err != nil {
return err
}
// Replace the full variable name (${variable}) with the actual secret value which will be fetched by the underlying driver.
secretValue, err := secret.Value()
if err != nil {
return err
}
sourceValue = strings.ReplaceAll(fmt.Sprint(sourceValue), secret.FullName(), secretValue)
data.SetPath(sourceValue, secret.Identifier...)
return nil
}
// Load is used to load the actual secret files and ensure that they are correctly formatted.
// Load does NOT load the actual value, it just ensures that it could be loaded using the secret.Value() call.
func (s *Secret) Load(fs afero.Fs) error {
if err := s.LoadSecretFileData(fs); err != nil {
return fmt.Errorf("failed to load secret file: %w", err)
}
return nil
}
// attemptCreate will attempt to use the AlternativeAction of a secret to create it and write the required secret file to the filesystem.
func (secret *Secret) attemptCreate(fs afero.Fs, secretPath string) error {
// if the secret does not have an alternative call, it is considered invalid and we cannot continue because we require the secret file to exist
if secret.AlternativeCall == nil {
return fmt.Errorf("secret does not exist and no alternative call is specified: %s in '%s'", secret.FullName(), secret.Path())
}
// call the given alternative call function to get the target output
output := secret.AlternativeCall.Execute()
// use the driver implementation to encrypt the secret data
encryptedData, err := secret.Driver.Encrypt(output)
if err != nil {
return fmt.Errorf("data encryption failed: %w", err)
}
// create new Data map which can then be written into the secret file
secretFileData, err := NewSecretData(encryptedData, secret.Driver.Type(), secret.Driver.GetKey())
if err != nil {
return fmt.Errorf("could not create NewSecretData: %w", err)
}
fileData, err := NewData(secretFileData)
if err != nil {
return fmt.Errorf("malformed SecretFileData cannot be converted to Data: %w", err)
}
// create the secret file with the data from the alternative action
secretFile, err := CreateNewYamlFile(fs, filepath.Join(secretPath, secret.RelativePath), fileData.Bytes())
if err != nil {
return err
}
secret.YamlFile = secretFile
return nil
}
// secretFindValueFunc implements the [FindValueFunc] and searches for secrets inside [Data].
// Secrets can be found by matching any value to the [secretRegex].
// All found secrets are initialized, matched agains the SecretFileList to ensure they exist and added to the output.
// The function returns `[]*String` which needs to be restored afterwards.
func secretFindValueFunc(secretFiles SecretFileList) FindValueFunc {
return func(value string, path []interface{}) (val interface{}, err error) {
var secrets []*Secret
matches := secretRegex.FindAllStringSubmatch(value, -1)
if len(matches) > 0 {
for _, secret := range matches {
if len(secret) >= 3 {
secretDriver := secret[1]
secretRelativePath := secret[2]
secretAlternativeAction := secret[4]
// in case the secret file does not (yet) exist, the secretFile will be nil
secretFile := secretFiles.GetSecretFile(secretRelativePath)
// if the secretFile is nil, the secret does not (yet) exist.
// we will need to create it further on, but store the relative path by creating an empty [SecretFile]
if secretFile == nil {
secretFile, err = NewSecretFile(nil, secretRelativePath)
if err != nil {
return nil, err
}
}
alternativeCall, valid, err := NewRawCall(secretAlternativeAction)
if err != nil {
return nil, err
}
// the call is not going to be executed if it is nil, thus we nil it here
if !valid {
alternativeCall = nil
}
newSecret, err := NewSecret(secretFile, secretDriver, alternativeCall, path)
if err != nil {
return nil, fmt.Errorf("invalid secret %s: %w", secret[0], err)
}
secrets = append(secrets, newSecret)
}
}
}
return secrets, nil
}
}
// secretYamlFileLoader returns a YamlFileLoaderFunc which is capable of
// creating SecretFiles from a given YamlFile.
// The created secret files are then appended to the passed secretFileList.
func secretYamlFileLoader(secretFileList *[]*SecretFile) YamlFileLoaderFunc {
return func(file *YamlFile, relativePath string) error {
secret, err := NewSecretFile(file, relativePath)
if err != nil {
return fmt.Errorf("%s: %w", file.Path, err)
}
(*secretFileList) = append((*secretFileList), secret)
return nil
}
}
// Value returns the actual secret value.
func (s *Secret) Value() (string, error) {
if s.Driver.GetKey() != s.Data.Key {
fmt.Fprintf(os.Stderr, "key in secret file '%s' differs from the key in the inventory\n", s.Path())
}
return s.Driver.Decrypt(s.Data.Data, s.Data.Key)
}
// FullName returns the full secret name as it would be expected to ocurr in a class/target.
func (s Secret) FullName() string {
if s.AlternativeCall != nil {
return fmt.Sprintf("?{%s:%s||%s}", s.DriverName, s.SecretFile.RelativePath, s.AlternativeCall.RawString())
} else {
return fmt.Sprintf("?{%s:%s}", s.DriverName, s.SecretFile.RelativePath)
}
}
func (s Secret) Path() string {
var segments []string
for _, seg := range s.Identifier {
segments = append(segments, fmt.Sprint(seg))
}
return strings.Join(segments, ".")
}