| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package config |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "reflect" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/BurntSushi/toml" |
| 11 | ) |
| 12 | |
| 13 | // secretFieldNames lists case-insensitive substrings that mark a field as |
| 14 | // a secret. Matches are deliberately broad — better to redact a non-secret |
| 15 | // than leak one. URL fields are included because connection URLs commonly |
| 16 | // carry credentials in the userinfo component. |
| 17 | var secretFieldNames = []string{ |
| 18 | "password", "pass", |
| 19 | "secret", |
| 20 | "key", |
| 21 | "token", |
| 22 | "dsn", |
| 23 | "url", |
| 24 | } |
| 25 | |
| 26 | // PrintRedacted writes the config to w in TOML form with secrets replaced |
| 27 | // by `***`. The transformation operates on a deep copy so the live Config |
| 28 | // is never mutated. |
| 29 | func PrintRedacted(c Config) (string, error) { |
| 30 | cp := redactCopy(reflect.ValueOf(c)).Interface().(Config) |
| 31 | var buf strings.Builder |
| 32 | if err := toml.NewEncoder(&buf).Encode(cp); err != nil { |
| 33 | return "", fmt.Errorf("config: encode: %w", err) |
| 34 | } |
| 35 | return buf.String(), nil |
| 36 | } |
| 37 | |
| 38 | func redactCopy(v reflect.Value) reflect.Value { |
| 39 | out := reflect.New(v.Type()).Elem() |
| 40 | switch v.Kind() { |
| 41 | case reflect.Struct: |
| 42 | for i := 0; i < v.NumField(); i++ { |
| 43 | fv := v.Field(i) |
| 44 | ft := v.Type().Field(i) |
| 45 | if !ft.IsExported() { |
| 46 | continue |
| 47 | } |
| 48 | if fv.Kind() == reflect.Struct { |
| 49 | out.Field(i).Set(redactCopy(fv)) |
| 50 | continue |
| 51 | } |
| 52 | if shouldRedact(ft.Name) && fv.Kind() == reflect.String && fv.String() != "" { |
| 53 | out.Field(i).SetString("***") |
| 54 | continue |
| 55 | } |
| 56 | out.Field(i).Set(fv) |
| 57 | } |
| 58 | default: |
| 59 | out.Set(v) |
| 60 | } |
| 61 | return out |
| 62 | } |
| 63 | |
| 64 | func shouldRedact(fieldName string) bool { |
| 65 | lower := strings.ToLower(fieldName) |
| 66 | for _, needle := range secretFieldNames { |
| 67 | if strings.Contains(lower, needle) { |
| 68 | return true |
| 69 | } |
| 70 | } |
| 71 | return false |
| 72 | } |
| 73 |