package config import ( "fmt" "time" ) // Overrides is the "flag" layer: whatever the user passed on the command // line. Every field is the zero value when not set; string "" and bool // pointer nil both mean "the flag layer says nothing about this field", // which is why the bool fields here are pointers (a flag can meaningfully // set skip-verify back to false) while the plainly additive string fields // are not. type Overrides struct { Profile string Address string Namespace string Token string CACert string ClientCert string ClientKey string SkipVerify *bool ReadOnly *bool NoEnv bool // --no-env: disable the env layer entirely for this run NoColor bool // --no-color: suppress ANSI color in the TUI } // layer is one candidate value plus where it would come from if chosen. type layer[T any] struct { val *T layer string key string } // pick returns the value (written into *dst) and Origin of the first layer // (in argument order, highest precedence first) whose val is non-nil. // Layers whose val is nil are "silent" and skipped. If no layer matches, // dst is left untouched and a zero Origin is returned. func pick[T any](dst *T, layers ...layer[T]) Origin { for _, l := range layers { if l.val != nil { *dst = *l.val return Origin{Layer: l.layer, Key: l.key} } } return Origin{} } func strLayer(v, layerName, key string) layer[string] { if v == "" { return layer[string]{} } return layer[string]{val: &v, layer: layerName, key: key} } // Resolve builds a Settings for profileName out of f (the loaded config // file), ov (the flag layer), and the process environment (or an injected // Getenv in tests). Precedence, for every field independently: // // flag > env > profile > defaults block > builtin default // // This function — not api.DefaultConfig()'s own ReadEnvironment — is the // single source of truth for that ordering. api.NewClient applies // VAULT_TOKEN/VAULT_NAMESPACE itself as soon as a *api.Config is handed to // it, which would silently place env above every other layer; internal/vault.New // undoes that (ClearToken + SetNamespace) immediately after construction so // only values that went through this function ever take effect. func Resolve(f *File, profileName string, ov Overrides, get Getenv) (*Settings, error) { if get == nil { get = OSEnviron } name, err := SelectProfile(f, profileName, get) if err != nil { return nil, err } prof := f.Profiles[name] if prof == nil { prof = &Profile{} } def := f.Defaults noEnv := ov.NoEnv if !noEnv { if b, ok := envBool(get, EnvTUINoEnv); ok { noEnv = b } } if prof.IgnoreEnv != nil && *prof.IgnoreEnv { noEnv = true } envGet := get if noEnv { envGet = func(string) (string, bool) { return "", false } } s := &Settings{Profile: name, Origins: map[string]Origin{}} set := func(field string, o Origin) { if o.Layer != "" { s.Origins[field] = o } } set("address", pick(&s.Address, strLayer(ov.Address, LayerFlag, "--address"), envStrLayer(envGet, EnvAddress), strLayer(prof.Address, LayerProfile, "profiles."+name+".address"), strLayer(def.Address, LayerDefaults, "defaults.address"), layer[string]{val: strp("https://127.0.0.1:8200"), layer: LayerBuiltin, key: "builtin"}, )) set("namespace", pick(&s.Namespace, strLayer(ov.Namespace, LayerFlag, "--namespace"), envStrLayer(envGet, EnvNamespace), strLayer(prof.Namespace, LayerProfile, "profiles."+name+".namespace"), strLayer(def.Namespace, LayerDefaults, "defaults.namespace"), )) set("ca_cert", pick(&s.CACert, strLayer(ov.CACert, LayerFlag, "--ca-cert"), envStrLayer(envGet, EnvCACert), strLayer(prof.TLS.CACert, LayerProfile, "profiles."+name+".tls.ca_cert"), strLayer(def.TLS.CACert, LayerDefaults, "defaults.tls.ca_cert"), )) if v, ok := envGet(EnvCACertBytes); ok && v != "" { s.CACertPEM = []byte(v) } set("ca_path", pick(&s.CAPath, envStrLayer(envGet, EnvCAPath), strLayer(prof.TLS.CAPath, LayerProfile, "profiles."+name+".tls.ca_path"), strLayer(def.TLS.CAPath, LayerDefaults, "defaults.tls.ca_path"), )) set("client_cert", pick(&s.ClientCert, strLayer(ov.ClientCert, LayerFlag, "--client-cert"), envStrLayer(envGet, EnvClientCert), strLayer(prof.TLS.ClientCert, LayerProfile, "profiles."+name+".tls.client_cert"), strLayer(def.TLS.ClientCert, LayerDefaults, "defaults.tls.client_cert"), )) set("client_key", pick(&s.ClientKey, strLayer(ov.ClientKey, LayerFlag, "--client-key"), envStrLayer(envGet, EnvClientKey), strLayer(prof.TLS.ClientKey, LayerProfile, "profiles."+name+".tls.client_key"), strLayer(def.TLS.ClientKey, LayerDefaults, "defaults.tls.client_key"), )) set("tls_server_name", pick(&s.ServerName, envStrLayer(envGet, EnvTLSServerName), strLayer(prof.TLS.ServerName, LayerProfile, "profiles."+name+".tls.tls_server_name"), strLayer(def.TLS.ServerName, LayerDefaults, "defaults.tls.tls_server_name"), )) set("skip_verify", pick(&s.SkipVerify, boolLayer(ov.SkipVerify, LayerFlag, "--tls-skip-verify"), envBoolLayer(envGet, EnvSkipVerify), boolLayer(prof.TLS.SkipVerify, LayerProfile, "profiles."+name+".tls.skip_verify"), boolLayer(def.TLS.SkipVerify, LayerDefaults, "defaults.tls.skip_verify"), )) set("timeout", pick(&s.Timeout, envDurationLayer(envGet, EnvClientTimeout), durationLayer(prof.Client.Timeout, LayerProfile, "profiles."+name+".client.timeout"), durationLayer(def.Client.Timeout, LayerDefaults, "defaults.client.timeout"), durationLayer(durp(60*time.Second), LayerBuiltin, "builtin"), )) set("max_retries", pick(&s.MaxRetries, envIntLayer(envGet, EnvMaxRetries), intLayer(prof.Client.MaxRetries, LayerProfile, "profiles."+name+".client.max_retries"), intLayer(def.Client.MaxRetries, LayerDefaults, "defaults.client.max_retries"), intLayer(intp(2), LayerBuiltin, "builtin"), )) set("min_retry_wait", pick(&s.MinRetryWait, durationLayer(prof.Client.MinRetryWait, LayerProfile, "profiles."+name+".client.min_retry_wait"), durationLayer(def.Client.MinRetryWait, LayerDefaults, "defaults.client.min_retry_wait"), )) set("max_retry_wait", pick(&s.MaxRetryWait, durationLayer(prof.Client.MaxRetryWait, LayerProfile, "profiles."+name+".client.max_retry_wait"), durationLayer(def.Client.MaxRetryWait, LayerDefaults, "defaults.client.max_retry_wait"), )) set("srv_lookup", pick(&s.SRVLookup, envBoolLayer(envGet, EnvSRVLookup), boolLayer(prof.Client.SRVLookup, LayerProfile, "profiles."+name+".client.srv_lookup"), boolLayer(def.Client.SRVLookup, LayerDefaults, "defaults.client.srv_lookup"), )) set("disable_redirects", pick(&s.DisableRedirects, envBoolLayer(envGet, EnvDisableRedirects), boolLayer(prof.Client.DisableRedirects, LayerProfile, "profiles."+name+".client.disable_redirects"), boolLayer(def.Client.DisableRedirects, LayerDefaults, "defaults.client.disable_redirects"), )) set("http_proxy", pick(&s.HTTPProxy, envStrLayer(envGet, EnvHTTPProxy), envStrLayer(envGet, EnvProxyAddr), strLayer(prof.Client.HTTPProxy, LayerProfile, "profiles."+name+".client.http_proxy"), strLayer(def.Client.HTTPProxy, LayerDefaults, "defaults.client.http_proxy"), )) set("rate_limit", pick(&s.RateLimit, envStrLayer(envGet, EnvRateLimit), strLayer(prof.Client.RateLimit, LayerProfile, "profiles."+name+".client.rate_limit"), strLayer(def.Client.RateLimit, LayerDefaults, "defaults.client.rate_limit"), )) s.Headers = mergeHeaders(def.Client.Headers, prof.Client.Headers) s.Auth = mergeAuth(def.Auth, prof.Auth) s.Token = mergeTokenOpts(def.Token, prof.Token) set("read_only", pick(&s.ReadOnly, boolLayer(ov.ReadOnly, LayerFlag, "--read-only"), envBoolLayer(get, EnvTUIReadOnly), // the read-only guard is NOT subject to --no-env boolLayer(prof.ReadOnly, LayerProfile, "profiles."+name+".read_only"), boolLayer(def.ReadOnly, LayerDefaults, "defaults.read_only"), boolLayer(f.UI.ReadOnly, LayerDefaults, "ui.read_only"), boolLayer(boolp(true), LayerBuiltin, "builtin"), // safe default: read-only until asked otherwise )) set("mask_values", pick(&s.MaskValues, boolLayer(f.UI.MaskValues, LayerDefaults, "ui.mask_values"), boolLayer(boolp(true), LayerBuiltin, "builtin"))) set("confirm_destructive", pick(&s.ConfirmDestructive, boolLayer(f.UI.ConfirmDestructive, LayerDefaults, "ui.confirm_destructive"), boolLayer(boolp(true), LayerBuiltin, "builtin"))) set("require_cas", pick(&s.RequireCAS, boolLayer(f.UI.RequireCAS, LayerDefaults, "ui.require_cas"), boolLayer(boolp(true), LayerBuiltin, "builtin"))) set("cache_ttl", pick(&s.CacheTTL, durationLayer(f.UI.CacheTTL, LayerDefaults, "ui.cache_ttl"), durationLayer(durp(60*time.Second), LayerBuiltin, "builtin"))) set("ttl_warn_below", pick(&s.TTLWarnBelow, durationLayer(f.UI.TTLWarnBelow, LayerDefaults, "ui.ttl_warn_below"), durationLayer(durp(5*time.Minute), LayerBuiltin, "builtin"))) set("clipboard_clear_after", pick(&s.ClipboardClear, durationLayer(f.UI.ClipboardClear, LayerDefaults, "ui.clipboard_clear_after"))) set("appearance", pick(&s.Appearance, strLayer(f.UI.Appearance, LayerDefaults, "ui.appearance"), strLayer("auto", LayerBuiltin, "builtin"))) s.BrowserCommand = f.UI.BrowserCommand // NO_COLOR (https://no-color.org) is a terminal-wide convention, not a // Vault setting, so — like read_only above — it's read from the raw // environment even under --no-env. s.NoColor = ov.NoColor if !s.NoColor { if _, isSet := get("NO_COLOR"); isSet { s.NoColor = true } } return s, nil } // SelectProfile applies the profile-selection precedence: --profile flag > // VAULT_TUI_PROFILE env > current_profile in the file > the sole profile if // exactly one exists > "default". func SelectProfile(f *File, flag string, get Getenv) (string, error) { if flag != "" { return flag, nil } if get == nil { get = OSEnviron } if v, ok := get(EnvTUIProfile); ok && v != "" { return v, nil } if f.CurrentProfile != "" { return f.CurrentProfile, nil } if len(f.Profiles) == 1 { for name := range f.Profiles { return name, nil } } if len(f.Profiles) == 0 { return "default", nil } return "", fmt.Errorf("multiple profiles configured and none selected: pass --profile, set %s, or set current_profile in the config file", EnvTUIProfile) } func mergeHeaders(a, b map[string]string) map[string]string { out := map[string]string{} for k, v := range a { out[k] = v } for k, v := range b { out[k] = v } return out } func mergeAuth(def, prof Auth) Auth { out := def if prof.Method != "" { out.Method = prof.Method } if prof.Mount != "" { out.Mount = prof.Mount } if len(prof.Params) > 0 { merged := map[string]string{} for k, v := range def.Params { merged[k] = v } for k, v := range prof.Params { merged[k] = v } out.Params = merged } out.OIDC = mergeOIDCOpts(def.OIDC, prof.OIDC) return out } func mergeOIDCOpts(def, prof OIDCOpts) OIDCOpts { out := def if prof.ListenAddress != "" { out.ListenAddress = prof.ListenAddress } if prof.Port != nil { out.Port = prof.Port } if prof.CallbackMethod != "" { out.CallbackMethod = prof.CallbackMethod } if prof.CallbackHost != "" { out.CallbackHost = prof.CallbackHost } if prof.CallbackPort != nil { out.CallbackPort = prof.CallbackPort } if prof.CallbackPath != "" { out.CallbackPath = prof.CallbackPath } if prof.SkipBrowser != nil { out.SkipBrowser = prof.SkipBrowser } if prof.AbortOnBrowserError != nil { out.AbortOnBrowserError = prof.AbortOnBrowserError } if prof.Timeout != nil { out.Timeout = prof.Timeout } return out } func mergeTokenOpts(def, prof TokenOpts) TokenOpts { out := def if prof.Storage != "" { out.Storage = prof.Storage } if prof.File != "" { out.File = prof.File } if prof.Value != "" { out.Value = prof.Value } if prof.MirrorToVaultCLI != nil { out.MirrorToVaultCLI = prof.MirrorToVaultCLI } if prof.AutoRenew != nil { out.AutoRenew = prof.AutoRenew } if prof.RenewIncrement != nil { out.RenewIncrement = prof.RenewIncrement } if prof.RevokeOnLogout != nil { out.RevokeOnLogout = prof.RevokeOnLogout } if prof.ValidateOnStartup != nil { out.ValidateOnStartup = prof.ValidateOnStartup } return out } // --- small helpers ----------------------------------------------------- func envStrLayer(get Getenv, key string) layer[string] { v, ok := get(key) if !ok || v == "" { return layer[string]{} } return layer[string]{val: &v, layer: LayerEnv, key: key} } func envBoolLayer(get Getenv, key string) layer[bool] { b, ok := envBool(get, key) if !ok { return layer[bool]{} } return layer[bool]{val: &b, layer: LayerEnv, key: key} } func envDurationLayer(get Getenv, key string) layer[time.Duration] { d, ok := envDuration(get, key) if !ok { return layer[time.Duration]{} } return layer[time.Duration]{val: &d, layer: LayerEnv, key: key} } func envIntLayer(get Getenv, key string) layer[int] { n, ok := envInt(get, key) if !ok { return layer[int]{} } return layer[int]{val: &n, layer: LayerEnv, key: key} } func boolLayer(v *bool, layerName, key string) layer[bool] { if v == nil { return layer[bool]{} } return layer[bool]{val: v, layer: layerName, key: key} } func durationLayer(v *time.Duration, layerName, key string) layer[time.Duration] { if v == nil { return layer[time.Duration]{} } return layer[time.Duration]{val: v, layer: layerName, key: key} } func intLayer(v *int, layerName, key string) layer[int] { if v == nil { return layer[int]{} } return layer[int]{val: v, layer: layerName, key: key} } func strp(s string) *string { return &s } func boolp(b bool) *bool { return &b } func intp(i int) *int { return &i } func durp(d time.Duration) *time.Duration { return &d }