/
config.go
356 lines (297 loc) · 9.61 KB
/
config.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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
package config
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/gitconfig"
)
const (
DefaultPasswordLength = 24
DefaultXKCDLength = 4
)
var (
envPrefix = "GOPASS_CONFIG"
systemConfig = "/etc/gopass/config"
)
type Level int
const (
None Level = iota
Env
Worktree
Local
Global
System
Preset
)
func newGitconfig() *gitconfig.Configs {
c := gitconfig.New()
c.Name = "gopass"
c.EnvPrefix = envPrefix
c.GlobalConfig = os.Getenv("GOPASS_CONFIG")
c.SystemConfig = systemConfig
return c
}
var defaults = map[string]string{
"core.autopush": "true",
"core.autosync": "true",
"core.cliptimeout": "45",
"core.exportkeys": "true",
"core.notifications": "true",
"pwgen.xkcd-lang": "en",
}
// Config is a gopass config handler.
type Config struct {
root *gitconfig.Configs
cfgs map[string]*gitconfig.Configs
}
// migrationOpts is a list of config options that were used by gopass
// and need to be migrated to a new name, it maps old name -> new name
// the keys are used in our documentation test to spot legacy options
// that are still used in our codebase.
var migrationOpts = map[string]string{
// migration done in v1.15.9
"core.showsafecontent": "show.safecontent",
"core.autoclip": "generate.autoclip",
"core.showautoclip": "show.autoclip",
}
// New initializes a new gopass config. It will handle legacy configs as well and legacy option names, migrating
// them to their new location and names on a best effort basis. Any system level config or env variables options are
// not migrated.
func New() *Config {
c := newWithOptions(false)
// we only migrate options when we are allowed to write them
c.migrateOptions(migrationOpts)
return c
}
// NewInMemory initializes a new config that does not allow writes. For use in tests.
// This does not migrate legacy option names to their correct config section.
func NewInMemory() *Config {
return newWithOptions(true)
}
// NewContextReadOnly returns a context with a read-only config.
func NewContextInMemory() context.Context {
return NewInMemory().WithConfig(context.Background())
}
func newWithOptions(noWrites bool) *Config {
c := &Config{
cfgs: make(map[string]*gitconfig.Configs, 42),
}
// if there is no per-user gitconfig we try to migrate
// an existing config. But we will leave it around for
// gopass fsck to (optionally) clean it up.
if nm := os.Getenv("GOPASS_CONFIG_NO_MIGRATE"); !HasGlobalConfig() && nm == "" {
if err := migrateConfigs(); err != nil {
debug.Log("failed to migrate from old config: %s", err)
}
}
// load the global config to get the root path
c.root = newGitconfig().LoadAll("")
c.root.NoWrites = noWrites
rootPath := c.root.Get("mounts.path")
if rootPath == "" {
if err := c.SetPath(PwStoreDir("")); err != nil {
debug.Log("failed to set path: %s", err)
}
}
// load again, this might add a per-store config from the root store
c.root.LoadAll(rootPath)
c.root.NoWrites = noWrites
if rootPath := c.root.Get("mounts.path"); rootPath == "" {
if err := c.SetPath(PwStoreDir("")); err != nil {
debug.Log("failed to set path: %s", err)
}
}
// set global defaults
c.root.Preset = gitconfig.NewFromMap(defaults)
for _, m := range c.Mounts() {
c.cfgs[m] = newGitconfig().LoadAll(c.MountPath(m))
c.cfgs[m].NoWrites = noWrites
}
return c
}
// HasGlobalConfig returns true if there is an existing global config.
func HasGlobalConfig() bool {
return newGitconfig().HasGlobalConfig()
}
// IsSet returns true if the key is set in the root config.
func (c *Config) IsSet(key string) bool {
return c.root.IsSet(key)
}
// IsSetM returns true if the key is set in the mount or the root config if mount is empty.
func (c *Config) IsSetM(mount, key string) bool {
if mount == "" || mount == "<root>" {
return c.root.IsSet(key)
}
if cfg := c.cfgs[mount]; cfg != nil {
return cfg.IsSet(key)
}
return false
}
// Get returns the given key from the root config.
func (c *Config) Get(key string) string {
return c.root.Get(key)
}
// GetAll returns all values for the given key.
func (c *Config) GetAll(key string) []string {
return c.root.GetAll(key)
}
// GetGlobal returns the given key from the root global config.
// This is typically used to prevent a local config override of sensitive config items, e.g. used for integrity checks.
func (c *Config) GetGlobal(key string) string {
return c.root.GetGlobal(key)
}
// GetM returns the given key from the mount or the root config if mount is empty.
func (c *Config) GetM(mount, key string) string {
if mount == "" || mount == "<root>" {
return c.root.Get(key)
}
if cfg := c.cfgs[mount]; cfg != nil {
return cfg.Get(key)
}
return ""
}
// Set tries to set the key to the given value.
// The mount option is necessary to discern between
// the per-user (global) and possible per-directory (local)
// config files.
//
// - If mount is empty the setting will be written to the per-user config (global)
// - If mount has the special value "<root>" the setting will be written to the per-directory config of the root store (local)
// - If mount has any other value we will attempt to write the setting to the per-directory config of this mount.
// - If the mount point does not exist we will return nil.
func (c *Config) Set(mount, key, value string) error {
_, err := c.SetWithLevel(mount, key, value)
return err
}
// SetWithLevel is the same as Set, but it also returns the level at which the config was set.
// It currently only supports global and local configs.
func (c *Config) SetWithLevel(mount, key, value string) (Level, error) {
if mount == "" {
return Global, c.root.SetGlobal(key, value)
}
if mount == "<root>" {
return Local, c.root.SetLocal(key, value)
}
if cfg, ok := c.cfgs[mount]; !ok {
return None, fmt.Errorf("substore %q is not initialized or doesn't exist", mount)
} else if cfg != nil {
return Local, cfg.SetLocal(key, value)
}
return None, nil
}
// SetEnv overrides a key in the non-persistent layer.
func (c *Config) SetEnv(key, value string) error {
return c.root.SetEnv(key, value)
}
// Path returns the root store path.
func (c *Config) Path() string {
return c.Get("mounts.path")
}
// MountPath returns the mount store path.
func (c *Config) MountPath(mountPoint string) string {
return c.Get(mpk(mountPoint))
}
// SetPath is a shortcut to set the root store path.
func (c *Config) SetPath(path string) error {
return c.Set("", "mounts.path", path)
}
// SetMountPath is a shortcut to set a mount to a path.
func (c *Config) SetMountPath(mount, path string) error {
return c.Set("", mpk(mount), path)
}
// mpk for mountPathKey.
func mpk(mount string) string {
return fmt.Sprintf("mounts.%s.path", mount)
}
// Mounts returns all mount points from the root config.
// Note: Any mounts in local configs are ignored.
func (c *Config) Mounts() []string {
return c.root.ListSubsections("mounts")
}
// Unset deletes the key from the given config.
func (c *Config) Unset(mount, key string) error {
if mount == "" {
return c.root.UnsetGlobal(key)
}
if mount == "<root>" {
return c.root.UnsetLocal(key)
}
if cfg := c.cfgs[mount]; cfg != nil {
return cfg.UnsetLocal(key)
}
return nil
}
// Keys returns all keys in the given config.
func (c *Config) Keys(mount string) []string {
if mount == "" || mount == "<root>" {
return c.root.Keys()
}
if cfg := c.cfgs[mount]; cfg != nil {
return cfg.Keys()
}
return nil
}
// migrateOptions is a best effort migration tool for when we introduce new options. It does not necessarily
// handle worktree and env level options very well.
func (c *Config) migrateOptions(migrations map[string]string) {
if nm := os.Getenv("GOPASS_CONFIG_NO_MIGRATE"); nm != "" {
return
}
var errs []error
debug.V(2).Log("migrateOptions running")
for oldK, newK := range migrations {
found := false
if val := c.root.GetGlobal(oldK); val != "" {
debug.V(2).Log("migrating option in root global store: %s -> %s ", oldK, newK)
errs = append(errs, c.root.SetGlobal(newK, val))
errs = append(errs, c.root.UnsetGlobal(oldK))
found = true
}
if val := c.root.GetLocal(oldK); val != "" {
debug.V(2).Log("migrating option in <root> local store: %s -> %s ", oldK, newK)
errs = append(errs, c.root.SetLocal(newK, val))
errs = append(errs, c.root.UnsetLocal(oldK))
found = true
}
for _, m := range c.Mounts() {
if cfg := c.cfgs[m]; cfg != nil {
if val := cfg.GetLocal(oldK); val != "" {
debug.V(2).Log("migrating option in local store %s: %s -> %s ", m, oldK, newK)
errs = append(errs, cfg.SetLocal(newK, val))
errs = append(errs, cfg.UnsetLocal(oldK))
found = true
}
if val := cfg.Get(oldK); !found && val != "" {
debug.V(2).Log("Found old option %s = %s in config, probably at the worktree or env level, "+
"or maybe at the system level cannot migrate it.", oldK, val)
}
}
}
}
if err := errors.Join(errs...); err != nil {
debug.Log("Errors encountered while migrating old options: {%v}", err)
}
}
// DefaultPasswordLengthFromEnv will determine the password length from the env variable
// GOPASS_PW_DEFAULT_LENGTH or fallback to the hard-coded default length.
// If the env variable is set by the user and is valid, the boolean return value
// will be true, otherwise it will be false.
func DefaultPasswordLengthFromEnv(ctx context.Context) (int, bool) {
def := DefaultPasswordLength
cfg, mp := FromContext(ctx)
if l := AsInt(cfg.GetM(mp, "generate.length")); l > 0 {
def = l
}
lengthStr, isSet := os.LookupEnv("GOPASS_PW_DEFAULT_LENGTH")
if !isSet {
return def, false
}
length, err := strconv.Atoi(lengthStr)
if err != nil || length < 1 {
return def, false
}
return length, true
}