Go · 1705 bytes Raw Blame History
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