-
-
Notifications
You must be signed in to change notification settings - Fork 812
/
Copy pathsecrets.go
224 lines (191 loc) · 5.03 KB
/
secrets.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
package cfg
import (
"errors"
"fmt"
"runtime"
"github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/logger"
)
type SecretLoadParams struct {
name string
globalConfig *config.Config
service string
secret *string
}
// Load module secrets.
//
// The credential helpers impose this structure:
//
// SERVICE is mapped to a SECRET and USERNAME
//
// Only SECRET is secret, SERVICE and USERNAME are not, so this
// API doesn't expose USERNAME.
//
// SERVICE was intended to be the URL of an API server, but
// for hosted services that do not have or need a configurable
// API server, its easier to just use the module name as the
// SERVICE:
//
// cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()
//
// The user will use the module name as the service, and the API key as
// the secret, for example:
//
// % wtfutil save-secret circleci
// Secret: ...
//
// If a module (such as pihole, jenkins, or github) might have multiple
// instantiations each using a different API service (with its own unique
// API key), then the module should use the API URL to lookup the secret.
// For example, for github:
//
// cfg.ModuleSecret(name, globalConfig, &settings.apiKey).
// Service(settings.baseURL).
// Load()
//
// The user will use the API URL as the service, and the API key as the
// secret, for example, with github configured as:
//
// -- config.yml
// mods:
// github:
// baseURL: "https://github.mycompany.com/api/v3"
// ...
//
// the secret must be saved as:
//
// % wtfutil save-secret https://github.mycompany.com/api/v3
// Secret: ...
//
// If baseURL is not set in the configuration it will be the modules
// default, and the SERVICE will default to the module name, "github",
// and the user must save the secret as:
//
// % wtfutil save-secret github
// Secret: ...
//
// Ideally, the individual module documentation would describe the
// SERVICE name to use to save the secret.
func ModuleSecret(name string, globalConfig *config.Config, secret *string) *SecretLoadParams {
return &SecretLoadParams{
name: name,
globalConfig: globalConfig,
secret: secret,
service: name, // Default the service to the module name
}
}
func (slp *SecretLoadParams) Service(service string) *SecretLoadParams {
if service != "" {
slp.service = service
}
return slp
}
func (slp *SecretLoadParams) Load() {
configureSecret(
slp.globalConfig,
slp.service,
slp.secret,
)
}
type Secret struct {
Service string
Secret string
Username string
Store string
}
func configureSecret(
globalConfig *config.Config,
service string,
secret *string,
) {
if service == "" {
return
}
if secret == nil {
return
}
// Don't overwrite the secret if it was configured with yaml
if *secret != "" {
return
}
cred, err := FetchSecret(globalConfig, service)
if err != nil {
logger.Log(fmt.Sprintf("Loading secret failed: %s", err.Error()))
return
}
if cred == nil {
// No secret store configued.
return
}
if secret != nil && *secret == "" {
*secret = cred.Secret
}
}
// Fetch secret for `service`. Service is customarily a URL, but can be any
// identifier uniquely used by wtf to identify the service, such as the name
// of the module. nil is returned if the secretStore global property is not
// present or the secret is not found in that store.
func FetchSecret(globalConfig *config.Config, service string) (*Secret, error) {
prog := newProgram(globalConfig)
if prog == nil {
// No secret store configured.
return nil, nil
}
cred, err := client.Get(prog.runner, service)
if err != nil {
return nil, fmt.Errorf("get %v from %v: %w", service, prog.store, err)
}
return &Secret{
Service: cred.ServerURL,
Secret: cred.Secret,
Username: cred.Username,
Store: prog.store,
}, nil
}
func StoreSecret(globalConfig *config.Config, secret *Secret) error {
prog := newProgram(globalConfig)
if prog == nil {
return errors.New("cannot store secrets: wtf.secretStore is not configured")
}
cred := &credentials.Credentials{
ServerURL: secret.Service,
Username: secret.Username,
Secret: secret.Secret,
}
// docker-credential requires a username, but it isn't necessary for
// all services. Use a default if a username was not set.
if cred.Username == "" {
cred.Username = "default"
}
err := client.Store(prog.runner, cred)
if err != nil {
return fmt.Errorf("store %v: %w", prog.store, err)
}
return nil
}
type program struct {
store string
runner client.ProgramFunc
}
func newProgram(globalConfig *config.Config) *program {
secretStore := globalConfig.UString("wtf.secretStore", "(none)")
if secretStore == "(none)" {
return nil
}
if secretStore == "" {
switch runtime.GOOS {
case "windows":
secretStore = "winrt"
case "darwin":
secretStore = "osxkeychain"
default:
secretStore = "secretservice"
}
}
return &program{
secretStore,
client.NewShellProgramFunc("docker-credential-" + secretStore),
}
}