fix: abort startup when a config key is explicitly set to null
An explicitly-null key (port: null, bare port:, debug: ~, metrics subkeys, and every other known key) previously fell through the ok/nil check in the strict getters and silently took the default, violating the no-silent-fallback rule and contradicting metrics: null which already aborted. validateKnownKeys now collects null-valued keys (top level and metrics subkeys) and aborts naming each one, and the strict getters and validateAllowlistHostsValue error on null instead of defaulting as defense in depth. This also replaces the unhelpful 'value <nil> is not a map of metrics settings' rendering for metrics: null with the null-specific message.
This commit is contained in:
@@ -132,10 +132,12 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
|
|||||||
|
|
||||||
// validateKnownKeys rejects configuration files containing keys the
|
// validateKnownKeys rejects configuration files containing keys the
|
||||||
// application does not understand, so typos fail at startup instead of
|
// application does not understand, so typos fail at startup instead of
|
||||||
// being silently ignored. The env section is permitted because
|
// being silently ignored, and rejects keys that are explicitly set to
|
||||||
|
// null: a null is a SET value, never an omission, so it must not
|
||||||
|
// silently take the default. The env section is permitted because
|
||||||
// smartconfig consumes it for environment variable injection.
|
// smartconfig consumes it for environment variable injection.
|
||||||
func validateKnownKeys(sc *smartconfig.Config) error {
|
func validateKnownKeys(sc *smartconfig.Config) error {
|
||||||
var unknown []string
|
var unknown, nullKeys []string
|
||||||
|
|
||||||
for key, value := range sc.Data() {
|
for key, value := range sc.Data() {
|
||||||
if !isKnownConfigKey(key) {
|
if !isKnownConfigKey(key) {
|
||||||
@@ -144,6 +146,12 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
nullKeys = append(nullKeys, key)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if key == "metrics" {
|
if key == "metrics" {
|
||||||
metricsMap, ok := value.(map[string]interface{})
|
metricsMap, ok := value.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -152,9 +160,15 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
|||||||
"metrics", value)
|
"metrics", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
for subkey := range metricsMap {
|
for subkey, subvalue := range metricsMap {
|
||||||
if subkey != "username" && subkey != "password" {
|
if subkey != "username" && subkey != "password" {
|
||||||
unknown = append(unknown, "metrics."+subkey)
|
unknown = append(unknown, "metrics."+subkey)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if subvalue == nil {
|
||||||
|
nullKeys = append(nullKeys, "metrics."+subkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,9 +180,29 @@ func validateKnownKeys(sc *smartconfig.Config) error {
|
|||||||
return fmt.Errorf("unknown config keys: %s", strings.Join(unknown, ", "))
|
return fmt.Errorf("unknown config keys: %s", strings.Join(unknown, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(nullKeys) > 0 {
|
||||||
|
sort.Strings(nullKeys)
|
||||||
|
|
||||||
|
if len(nullKeys) == 1 {
|
||||||
|
return errNullConfigValue(nullKeys[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"config keys %s: value is null; omit a key entirely to use its default",
|
||||||
|
strings.Join(nullKeys, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errNullConfigValue reports a config key that is explicitly set to
|
||||||
|
// null (including the bare "key:" form and the "~" alias). Silently
|
||||||
|
// applying the default would mask a truncated or typo'd config entry.
|
||||||
|
func errNullConfigValue(key string) error {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"config key %q: value is null; omit the key entirely to use the default", key)
|
||||||
|
}
|
||||||
|
|
||||||
// isKnownConfigKey reports whether key is a permitted top-level
|
// isKnownConfigKey reports whether key is a permitted top-level
|
||||||
// configuration key.
|
// configuration key.
|
||||||
func isKnownConfigKey(key string) bool {
|
func isKnownConfigKey(key string) bool {
|
||||||
@@ -371,17 +405,22 @@ func (l *strictLoader) boolVal(key string, defaultVal bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getString returns the string value for key, or defaultVal if the key
|
// getString returns the string value for key, or defaultVal if the key
|
||||||
// is omitted. A present value that is not a string is an error.
|
// is omitted. A present value that is not a string, or is explicitly
|
||||||
|
// null, is an error.
|
||||||
func getString(sc *smartconfig.Config, key, defaultVal string) (string, error) {
|
func getString(sc *smartconfig.Config, key, defaultVal string) (string, error) {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, ok := sc.Get(key)
|
raw, ok := sc.Get(key)
|
||||||
if !ok || raw == nil {
|
if !ok {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if raw == nil {
|
||||||
|
return "", errNullConfigValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
str, ok := raw.(string)
|
str, ok := raw.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("config key %q: value %v (%T) is not a string",
|
return "", fmt.Errorf("config key %q: value %v (%T) is not a string",
|
||||||
@@ -392,18 +431,22 @@ func getString(sc *smartconfig.Config, key, defaultVal string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getInt returns the integer value for key, or defaultVal if the key is
|
// getInt returns the integer value for key, or defaultVal if the key is
|
||||||
// omitted. A present value that is not a whole number is an error;
|
// omitted. A present value that is not a whole number, or is explicitly
|
||||||
// fractional values are never truncated.
|
// null, is an error; fractional values are never truncated.
|
||||||
func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, ok := sc.Get(key)
|
raw, ok := sc.Get(key)
|
||||||
if !ok || raw == nil {
|
if !ok {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if raw == nil {
|
||||||
|
return 0, errNullConfigValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
switch val := raw.(type) {
|
switch val := raw.(type) {
|
||||||
case int:
|
case int:
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -430,17 +473,22 @@ func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
|||||||
|
|
||||||
// getBool returns the boolean value for key, or defaultVal if the key
|
// getBool returns the boolean value for key, or defaultVal if the key
|
||||||
// is omitted. A present value that is not a boolean (or a ParseBool-able
|
// is omitted. A present value that is not a boolean (or a ParseBool-able
|
||||||
// string) is an error; numbers are not accepted as booleans.
|
// string), or is explicitly null, is an error; numbers are not accepted
|
||||||
|
// as booleans.
|
||||||
func getBool(sc *smartconfig.Config, key string, defaultVal bool) (bool, error) {
|
func getBool(sc *smartconfig.Config, key string, defaultVal bool) (bool, error) {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, ok := sc.Get(key)
|
raw, ok := sc.Get(key)
|
||||||
if !ok || raw == nil {
|
if !ok {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if raw == nil {
|
||||||
|
return false, errNullConfigValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
switch val := raw.(type) {
|
switch val := raw.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -459,17 +507,21 @@ func getBool(sc *smartconfig.Config, key string, defaultVal bool) (bool, error)
|
|||||||
|
|
||||||
// validateAllowlistHostsValue checks the raw shape of the
|
// validateAllowlistHostsValue checks the raw shape of the
|
||||||
// allowlist_hosts value before the lenient extraction in getStringSlice
|
// allowlist_hosts value before the lenient extraction in getStringSlice
|
||||||
// runs: a value that is not a list of strings (or a comma-separated
|
// runs: an explicitly null value, a value that is not a list of strings
|
||||||
// string), a non-string entry, or an empty entry is an error, never
|
// (or a comma-separated string), a non-string entry, or an empty entry
|
||||||
// silently skipped.
|
// is an error, never silently skipped.
|
||||||
func validateAllowlistHostsValue(sc *smartconfig.Config) error {
|
func validateAllowlistHostsValue(sc *smartconfig.Config) error {
|
||||||
const key = "allowlist_hosts"
|
const key = "allowlist_hosts"
|
||||||
|
|
||||||
raw, ok := sc.Get(key)
|
raw, ok := sc.Get(key)
|
||||||
if !ok || raw == nil {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if raw == nil {
|
||||||
|
return errNullConfigValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
switch val := raw.(type) {
|
switch val := raw.(type) {
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
for _, item := range val {
|
for _, item := range val {
|
||||||
|
|||||||
Reference in New Issue
Block a user