diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 0c5465c6f..176fd00c3 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "reflect" "strings" "github.com/creasty/defaults" @@ -13,6 +14,7 @@ import ( "github.com/opentdf/platform/service/internal/server" "github.com/opentdf/platform/service/pkg/db" "github.com/opentdf/platform/service/pkg/serviceregistry" + "github.com/opentdf/platform/service/pkg/util" "github.com/spf13/viper" ) @@ -85,3 +87,21 @@ func LoadConfig(key string, file string) (*Config, error) { return config, nil } + +func (c *Config) LogValue() slog.Value { + redactedConfig := util.RedactSensitiveData(c) + var values []slog.Attr + v := reflect.ValueOf(redactedConfig).Elem() + t := v.Type() + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + key := fieldType.Tag.Get("yaml") + if key == "" { + key = fieldType.Name + } + values = append(values, slog.String(key, util.StructToString(field))) + } + return slog.GroupValue(values...) +} diff --git a/service/pkg/db/db.go b/service/pkg/db/db.go index 6764badb6..f84f7385d 100644 --- a/service/pkg/db/db.go +++ b/service/pkg/db/db.go @@ -66,7 +66,7 @@ type Config struct { Port int `yaml:"port" default:"5432"` Database string `yaml:"database" default:"opentdf"` User string `yaml:"user" default:"postgres"` - Password string `yaml:"password" default:"changeme"` + Password string `yaml:"password" default:"changeme" secret:"true"` RunMigrations bool `yaml:"runMigrations" default:"true"` SSLMode string `yaml:"sslmode" default:"prefer"` Schema string `yaml:"schema" default:"opentdf"` diff --git a/service/pkg/util/redact.go b/service/pkg/util/redact.go new file mode 100644 index 000000000..5148f5397 --- /dev/null +++ b/service/pkg/util/redact.go @@ -0,0 +1,106 @@ +package util + +import ( + "fmt" + "reflect" + "strings" +) + +func RedactSensitiveData(i interface{}) interface{} { + v := reflect.ValueOf(i) + redacted := redact(v) + return redacted.Interface() +} + +func redact(v reflect.Value) reflect.Value { + //nolint:exhaustive // default case covers other type + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return v + } + redacted := reflect.New(v.Elem().Type()) + redacted.Elem().Set(redact(v.Elem())) + return redacted + case reflect.Struct: + redacted := reflect.New(v.Type()).Elem() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := v.Type().Field(i) + tag := fieldType.Tag.Get("secret") + if tag == "true" { + // Redact sensitive fields + redacted.Field(i).SetString("***") + } else { + // Recursively redact nested fields + redacted.Field(i).Set(redact(field)) + } + } + return redacted + case reflect.Map: + redacted := reflect.MakeMap(v.Type()) + for _, key := range v.MapKeys() { + val := v.MapIndex(key) + redacted.SetMapIndex(key, redact(val)) + } + return redacted + case reflect.Slice: + redacted := reflect.MakeSlice(v.Type(), v.Len(), v.Cap()) + for i := 0; i < v.Len(); i++ { + redacted.Index(i).Set(redact(v.Index(i))) + } + return redacted + default: + return v + } +} + +func StructToString(v reflect.Value) string { + var b strings.Builder + //nolint:exhaustive // default case covers other type + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return "" + } + return StructToString(v.Elem()) + case reflect.Struct: + b.WriteString("{") + t := v.Type() + for i := 0; i < v.NumField(); i++ { + if i > 0 { + b.WriteString(" ") + } + field := v.Field(i) + fieldType := t.Field(i) + b.WriteString(fieldType.Name) + b.WriteString(":") + b.WriteString(StructToString(field)) + } + b.WriteString("}") + return b.String() + case reflect.Map: + b.WriteString("map[") + keys := v.MapKeys() + for i, key := range keys { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(fmt.Sprintf("%v:%v", key, StructToString(v.MapIndex(key)))) + } + b.WriteString("]") + return b.String() + case reflect.Slice: + b.WriteString("[") + for i := 0; i < v.Len(); i++ { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(StructToString(v.Index(i))) + } + b.WriteString("]") + return b.String() + default: + return fmt.Sprintf("%v", v.Interface()) + } +} diff --git a/service/pkg/util/redact_test.go b/service/pkg/util/redact_test.go new file mode 100644 index 000000000..acaab76bc --- /dev/null +++ b/service/pkg/util/redact_test.go @@ -0,0 +1,126 @@ +package util + +import ( + "encoding/json" + "testing" +) + +type DBConfig struct { + Host string `json:"host"` + Port int `json:"port"` + Database string `json:"database"` + User string `json:"user"` + Password string `json:"password" secret:"true"` + RunMigrations bool `json:"runMigrations"` + SSLMode string `json:"sslmode"` + Schema string `json:"schema"` + VerifyConnection bool `json:"verifyConnection"` +} + +type Config struct { + DevMode bool `json:"devMode"` + DB DBConfig `json:"db"` + Services map[string]struct { + Enabled bool `json:"enabled"` + Remote struct { + Endpoint string `json:"endpoint"` + } `json:"remote"` + ExtraProps map[string]interface{} `json:"extraProps"` + } `json:"services"` +} + +func TestRedactSensitiveData_WithSensitiveFieldsInNestedStruct(t *testing.T) { + rawConfig := `{ + "DevMode": false, + "DB": { + "Host": "localhost", + "Port": 5432, + "Database": "opentdf", + "User": "postgres", + "Password": "changeme", + "RunMigrations": true, + "SSLMode": "prefer", + "Schema": "opentdf", + "VerifyConnection": true + }, + "Services": { + "authorization": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": { + "clientid": "tdf-authorization-svc", + "clientsecret": "secret", + "ersurl": "http://localhost:8080/entityresolution/resolve", + "tokenendpoint": "http://localhost:8888/auth/realms/opentdf/protocol/openid-connect/token" + } + }, + "entityresolution": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": { + "clientid": "tdf-entity-resolution", + "clientsecret": "secret", + "legacykeycloak": true, + "realm": "opentdf", + "url": "http://localhost:8888/auth" + } + }, + "health": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": {} + }, + "kas": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": { + "keyring": [ + {"alg": "ec:secp256r1", "kid": "e1"}, + {"alg": "ec:secp256r1", "kid": "e1", "legacy": true}, + {"alg": "rsa:2048", "kid": "r1"}, + {"alg": "rsa:2048", "kid": "r1", "legacy": true} + ] + } + }, + "policy": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": {} + }, + "wellknown": { + "Enabled": true, + "Remote": { + "Endpoint": "" + }, + "ExtraProps": {} + } + } + }` + + var config Config + err := json.Unmarshal([]byte(rawConfig), &config) + if err != nil { + t.Fatalf("Failed to unmarshal rawConfig: %v", err) + } + + redacted := RedactSensitiveData(config) + + redactedConfig, ok1 := redacted.(Config) + if !ok1 { + t.Fatalf("Expected redacted data to be of type Config") + } + + if redactedConfig.DB.Password != "***" { + t.Errorf("Expected DB.Password to be redacted") + } +}