[MM-63760] Only partially sanitize DB datasources for Support Packet (#30728)

Co-authored-by: Claude <noreply@anthropic.com>
Этот коммит содержится в:
Ben Schumacher
2025-06-06 15:07:54 +02:00
коммит произвёл GitHub
родитель 160cb91ab9
Коммит 5b389c5224
11 изменённых файлов: 183 добавлений и 111 удалений

Просмотреть файл

@@ -226,11 +226,11 @@ func (a *App) SanitizedConfig(cfg *model.Config) {
if err != nil { if err != nil {
// GetPluginManifests might error, e.g. when plugins are disabled. // GetPluginManifests might error, e.g. when plugins are disabled.
// Sanitize all plugin settings in this case. // Sanitize all plugin settings in this case.
cfg.Sanitize(nil) cfg.Sanitize(nil, nil)
return return
} }
cfg.Sanitize(manifests) cfg.Sanitize(manifests, nil)
} }
// GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable. // GetEnvironmentConfig returns a map of configuration keys whose values have been overridden by an environment variable.

Просмотреть файл

@@ -42,7 +42,7 @@ func (ps *PlatformService) Config() *model.Config {
} }
// getSanitizedConfig gets the configuration without any secrets. // getSanitizedConfig gets the configuration without any secrets.
func (ps *PlatformService) getSanitizedConfig(rctx request.CTX) *model.Config { func (ps *PlatformService) getSanitizedConfig(rctx request.CTX, opts *model.SanitizeOptions) *model.Config {
cfg := ps.Config().Clone() cfg := ps.Config().Clone()
manifests, err := ps.getPluginManifests() manifests, err := ps.getPluginManifests()
@@ -50,9 +50,9 @@ func (ps *PlatformService) getSanitizedConfig(rctx request.CTX) *model.Config {
// getPluginManifests might error, e.g. when plugins are disabled. // getPluginManifests might error, e.g. when plugins are disabled.
// Sanitize all plugin settings in this case. // Sanitize all plugin settings in this case.
rctx.Logger().Warn("Failed to get plugin manifests for config sanitization. Will sanitize all plugin settings.", mlog.Err(err)) rctx.Logger().Warn("Failed to get plugin manifests for config sanitization. Will sanitize all plugin settings.", mlog.Err(err))
cfg.Sanitize(nil) cfg.Sanitize(nil, opts)
} else { } else {
cfg.Sanitize(manifests) cfg.Sanitize(manifests, opts)
} }
return cfg return cfg

Просмотреть файл

@@ -196,7 +196,7 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
} }
func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) { func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) {
config := ps.getSanitizedConfig(rctx) config := ps.getSanitizedConfig(rctx, &model.SanitizeOptions{PartiallyRedactDataSources: true})
spConfig := model.SupportPacketConfig{ spConfig := model.SupportPacketConfig{
Config: config, Config: config,
FeatureFlags: *config.FeatureFlags, FeatureFlags: *config.FeatureFlags,

Просмотреть файл

@@ -360,13 +360,18 @@ func TestGetSanitizedConfigFile(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Ensure sensitive fields are redacted // Ensure sensitive fields are redacted
assert.Equal(t, model.FakeSetting, *config.SqlSettings.DataSource) assert.Equal(t, model.FakeSetting, *config.FileSettings.PublicLinkSalt)
// Ensure non-sensitive fields are present // Ensure non-sensitive fields are present
assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections) assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections)
// Ensure feature flags are present // Ensure feature flags are present
assert.Equal(t, "true", config.FeatureFlags.TestFeature) assert.Equal(t, "true", config.FeatureFlags.TestFeature)
// Ensure DataSource is partially sanitized (not completely replaced with FakeSetting)
// The default test database connection string should have username/password redacted
assert.Contains(t, *config.SqlSettings.DataSource, "****:****")
assert.NotEqual(t, model.FakeSetting, *config.SqlSettings.DataSource)
} }
func TestGetCPUProfile(t *testing.T) { func TestGetCPUProfile(t *testing.T) {

Просмотреть файл

@@ -409,7 +409,7 @@ func (ds *DatabaseStore) RemoveFile(name string) error {
func (ds *DatabaseStore) String() string { func (ds *DatabaseStore) String() string {
// This is called during the running of MM, so we expect the parsing of DSN // This is called during the running of MM, so we expect the parsing of DSN
// to be successful. // to be successful.
sanitized, _ := sqlUtils.SanitizeDataSource(ds.driverName, ds.originalDsn) sanitized, _ := model.SanitizeDataSource(ds.driverName, ds.originalDsn)
return sanitized return sanitized
} }

Просмотреть файл

@@ -65,19 +65,19 @@ func (cd ConfigDiffs) Sanitize() ConfigDiffs {
cfgPtr, ok := cd[0].BaseVal.(*model.Config) cfgPtr, ok := cd[0].BaseVal.(*model.Config)
if ok { if ok {
cfgPtr.Sanitize(pluginManifests) cfgPtr.Sanitize(pluginManifests, nil)
} }
cfgPtr, ok = cd[0].ActualVal.(*model.Config) cfgPtr, ok = cd[0].ActualVal.(*model.Config)
if ok { if ok {
cfgPtr.Sanitize(pluginManifests) cfgPtr.Sanitize(pluginManifests, nil)
} }
cfgVal, ok := cd[0].BaseVal.(model.Config) cfgVal, ok := cd[0].BaseVal.(model.Config)
if ok { if ok {
cfgVal.Sanitize(pluginManifests) cfgVal.Sanitize(pluginManifests, nil)
} }
cfgVal, ok = cd[0].ActualVal.(model.Config) cfgVal, ok = cd[0].ActualVal.(model.Config)
if ok { if ok {
cfgVal.Sanitize(pluginManifests) cfgVal.Sanitize(pluginManifests, nil)
} }
} }

Просмотреть файл

@@ -113,7 +113,7 @@ func TestDiffSanitized(t *testing.T) {
Path: "", Path: "",
BaseVal: func() model.Config { BaseVal: func() model.Config {
cfg := defaultConfigGen() cfg := defaultConfigGen()
cfg.Sanitize(nil) cfg.Sanitize(nil, nil)
return *cfg return *cfg
}(), }(),
ActualVal: model.Config{}, ActualVal: model.Config{},
@@ -131,7 +131,7 @@ func TestDiffSanitized(t *testing.T) {
BaseVal: model.Config{}, BaseVal: model.Config{},
ActualVal: func() model.Config { ActualVal: func() model.Config {
cfg := defaultConfigGen() cfg := defaultConfigGen()
cfg.Sanitize(nil) cfg.Sanitize(nil, nil)
return *cfg return *cfg
}(), }(),
}, },

Просмотреть файл

@@ -20,7 +20,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-sql-driver/mysql"
"github.com/mattermost/ldap" "github.com/mattermost/ldap"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/public/utils"
@@ -73,6 +75,9 @@ const (
FakeSetting = "********************************" FakeSetting = "********************************"
// SanitizedPassword is the placeholder used for redacting passwords in data sources
SanitizedPassword = "****"
RestrictEmojiCreationAll = "all" RestrictEmojiCreationAll = "all"
RestrictEmojiCreationAdmin = "admin" RestrictEmojiCreationAdmin = "admin"
RestrictEmojiCreationSystemAdmin = "system_admin" RestrictEmojiCreationSystemAdmin = "system_admin"
@@ -4681,6 +4686,14 @@ func (s *ImageProxySettings) isValid() *AppError {
return nil return nil
} }
// SanitizeOptions specifies options for the [Config.Sanitize] method.
type SanitizeOptions struct {
// PartiallyRedactDataSources, when true, only redacts usernames and passwords
// from data sources, keeping other connection parameters visible.
// When false, replaces the entire data source with FakeSetting.
PartiallyRedactDataSources bool
}
func (o *Config) GetSanitizeOptions() map[string]bool { func (o *Config) GetSanitizeOptions() map[string]bool {
options := map[string]bool{} options := map[string]bool{}
options["fullname"] = *o.PrivacySettings.ShowFullName options["fullname"] = *o.PrivacySettings.ShowFullName
@@ -4689,7 +4702,32 @@ func (o *Config) GetSanitizeOptions() map[string]bool {
return options return options
} }
func (o *Config) Sanitize(pluginManifests []*Manifest) { // Sanitize removes sensitive information from the configuration object.
// It replaces sensitive fields with [FakeSetting] or sanitizes them.
//
// Parameters:
// - pluginManifests: Plugin manifests for sanitizing plugin settings.
// - opts: Options for controlling sanitization behavior. If nil, defaults are used. See [SanitizeOptions].
func (o *Config) Sanitize(pluginManifests []*Manifest, opts *SanitizeOptions) {
if opts == nil {
opts = &SanitizeOptions{}
}
var driverName string
if o.SqlSettings.DriverName != nil {
driverName = *o.SqlSettings.DriverName
}
sanitizeDataSourceField := func(dataSource string, fieldName string) string {
if opts.PartiallyRedactDataSources && driverName != "" {
sanitized, err := SanitizeDataSource(driverName, dataSource)
if err != nil {
mlog.Warn("Failed to sanitize "+fieldName+". Falling back to fully sanitizing the setting.", mlog.Err(err))
return FakeSetting
}
return sanitized
}
return FakeSetting
}
if o.LdapSettings.BindPassword != nil && *o.LdapSettings.BindPassword != "" { if o.LdapSettings.BindPassword != nil && *o.LdapSettings.BindPassword != "" {
*o.LdapSettings.BindPassword = FakeSetting *o.LdapSettings.BindPassword = FakeSetting
} }
@@ -4723,7 +4761,7 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) {
} }
if o.SqlSettings.DataSource != nil { if o.SqlSettings.DataSource != nil {
*o.SqlSettings.DataSource = FakeSetting *o.SqlSettings.DataSource = sanitizeDataSourceField(*o.SqlSettings.DataSource, "SqlSettings.DataSource")
} }
if o.SqlSettings.AtRestEncryptKey != nil { if o.SqlSettings.AtRestEncryptKey != nil {
@@ -4735,15 +4773,18 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) {
} }
for i := range o.SqlSettings.DataSourceReplicas { for i := range o.SqlSettings.DataSourceReplicas {
o.SqlSettings.DataSourceReplicas[i] = FakeSetting o.SqlSettings.DataSourceReplicas[i] = sanitizeDataSourceField(o.SqlSettings.DataSourceReplicas[i], "SqlSettings.DataSourceReplicas")
} }
for i := range o.SqlSettings.DataSourceSearchReplicas { for i := range o.SqlSettings.DataSourceSearchReplicas {
o.SqlSettings.DataSourceSearchReplicas[i] = FakeSetting o.SqlSettings.DataSourceSearchReplicas[i] = sanitizeDataSourceField(o.SqlSettings.DataSourceSearchReplicas[i], "SqlSettings.DataSourceSearchReplicas")
} }
for i := range o.SqlSettings.ReplicaLagSettings { for i := range o.SqlSettings.ReplicaLagSettings {
o.SqlSettings.ReplicaLagSettings[i].DataSource = NewPointer(FakeSetting) if o.SqlSettings.ReplicaLagSettings[i].DataSource != nil {
sanitized := sanitizeDataSourceField(*o.SqlSettings.ReplicaLagSettings[i].DataSource, "SqlSettings.ReplicaLagSettings")
o.SqlSettings.ReplicaLagSettings[i].DataSource = NewPointer(sanitized)
}
} }
if o.MessageExportSettings.GlobalRelaySettings != nil && if o.MessageExportSettings.GlobalRelaySettings != nil &&
@@ -4763,6 +4804,59 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) {
o.PluginSettings.Sanitize(pluginManifests) o.PluginSettings.Sanitize(pluginManifests)
} }
// SanitizeDataSource redacts sensitive information (username and password) from a database
// connection string while preserving other connection parameters.
//
// Parameters:
// - driverName: The database driver name (postgres or mysql)
// - dataSource: The database connection string to sanitize
//
// Returns:
// - The sanitized connection string with username/password replaced by SanitizedPassword
// - An error if the driverName is not supported or if parsing fails
//
// Examples:
// - PostgreSQL: "postgres://user:pass@host:5432/db" -> "postgres://****:****@host:5432/db"
// - MySQL: "user:pass@tcp(host:3306)/db" -> "****:****@tcp(host:3306)/db"
func SanitizeDataSource(driverName, dataSource string) (string, error) {
// Handle empty data source
if dataSource == "" {
return "", nil
}
switch driverName {
case DatabaseDriverPostgres:
u, err := url.Parse(dataSource)
if err != nil {
return "", err
}
u.User = url.UserPassword(SanitizedPassword, SanitizedPassword)
// Remove username and password from query string
params := u.Query()
params.Del("user")
params.Del("password")
u.RawQuery = params.Encode()
// Unescape the URL to make it human-readable
out, err := url.QueryUnescape(u.String())
if err != nil {
return "", err
}
return out, nil
case DatabaseDriverMysql:
cfg, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
cfg.User = SanitizedPassword
cfg.Passwd = SanitizedPassword
return cfg.FormatDSN(), nil
default:
return "", errors.New("invalid drivername. Not postgres or mysql.")
}
}
type FilterTag struct { type FilterTag struct {
TagType string TagType string
TagName string TagName string

Просмотреть файл

@@ -1521,7 +1521,7 @@ func TestConfigSanitize(t *testing.T) {
QueryTimeLag: NewPointer("QueryTimeLag"), QueryTimeLag: NewPointer("QueryTimeLag"),
}} }}
c.Sanitize(nil) c.Sanitize(nil, nil)
assert.Equal(t, FakeSetting, *c.LdapSettings.BindPassword) assert.Equal(t, FakeSetting, *c.LdapSettings.BindPassword)
assert.Equal(t, FakeSetting, *c.FileSettings.PublicLinkSalt) assert.Equal(t, FakeSetting, *c.FileSettings.PublicLinkSalt)
@@ -1543,10 +1543,20 @@ func TestConfigSanitize(t *testing.T) {
t.Run("with default config", func(t *testing.T) { t.Run("with default config", func(t *testing.T) {
c := Config{} c := Config{}
c.SetDefaults() c.SetDefaults()
c.Sanitize(nil) c.Sanitize(nil, nil)
assert.Len(t, c.SqlSettings.ReplicaLagSettings, 0) assert.Len(t, c.SqlSettings.ReplicaLagSettings, 0)
}) })
t.Run("partially sanitize DataSource", func(t *testing.T) {
c := Config{}
c.SetDefaults()
*c.SqlSettings.DataSource = "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable"
c.Sanitize(nil, &SanitizeOptions{PartiallyRedactDataSources: true})
expectedURL := "postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost:5432/mattermost_test?sslmode=disable"
assert.Equal(t, expectedURL, *c.SqlSettings.DataSource)
})
} }
func TestPluginSettingsSanitize(t *testing.T) { func TestPluginSettingsSanitize(t *testing.T) {
@@ -1689,6 +1699,56 @@ func TestPluginSettingsSanitize(t *testing.T) {
} }
} }
func TestSanitizeDataSource(t *testing.T) {
t.Run(DatabaseDriverPostgres, func(t *testing.T) {
testCases := []struct {
Original string
Sanitized string
}{
{
"",
"",
},
{
"postgres://mmuser:mostest@localhost",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost",
},
{
"postgres://mmuser:mostest@localhost/dummy?sslmode=disable",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost/dummy?sslmode=disable",
},
{
"postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost/dummy?sslmode=disable",
},
}
driver := DatabaseDriverPostgres
for _, tc := range testCases {
out, err := SanitizeDataSource(driver, tc.Original)
require.NoError(t, err)
assert.Equal(t, tc.Sanitized, out)
}
})
t.Run(DatabaseDriverMysql, func(t *testing.T) {
testCases := []struct {
Original string
Sanitized string
}{
{
"mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s",
SanitizedPassword + ":" + SanitizedPassword + "@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s",
},
}
driver := DatabaseDriverMysql
for _, tc := range testCases {
out, err := SanitizeDataSource(driver, tc.Original)
require.NoError(t, err)
assert.Equal(t, tc.Sanitized, out)
}
})
}
func TestConfigFilteredByTag(t *testing.T) { func TestConfigFilteredByTag(t *testing.T) {
c := Config{} c := Config{}
c.SetDefaults() c.SetDefaults()
@@ -2146,7 +2206,7 @@ func TestFilterConfig(t *testing.T) {
require.NotEmpty(t, m) require.NotEmpty(t, m)
require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"]) require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil) cfg.Sanitize(nil, nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{ m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{ GetConfigOptions: GetConfigOptions{
RemoveDefaults: true, RemoveDefaults: true,
@@ -2156,7 +2216,7 @@ func TestFilterConfig(t *testing.T) {
require.NotEmpty(t, m) require.NotEmpty(t, m)
require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"]) require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil) cfg.Sanitize(nil, nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{ m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{ GetConfigOptions: GetConfigOptions{
RemoveDefaults: true, RemoveDefaults: true,

Просмотреть файл

@@ -6,7 +6,6 @@ package sql
import ( import (
"context" "context"
dbsql "database/sql" dbsql "database/sql"
"net/url"
"strings" "strings"
"time" "time"
@@ -57,7 +56,7 @@ func SetupConnection(logger mlog.LoggerIFace, connType string, dataSource string
} }
// At this point, we have passed sql.Open, so we deliberately ignore any errors. // At this point, we have passed sql.Open, so we deliberately ignore any errors.
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource) sanitized, _ := model.SanitizeDataSource(*settings.DriverName, dataSource)
logger = logger.With( logger = logger.With(
mlog.String("database", connType), mlog.String("database", connType),
@@ -101,37 +100,3 @@ func SetupConnection(logger mlog.LoggerIFace, connType string, dataSource string
return db, nil return db, nil
} }
func SanitizeDataSource(driverName, dataSource string) (string, error) {
switch driverName {
case model.DatabaseDriverPostgres:
u, err := url.Parse(dataSource)
if err != nil {
return "", err
}
u.User = url.UserPassword("****", "****")
// Remove username and password from query string
params := u.Query()
params.Del("user")
params.Del("password")
u.RawQuery = params.Encode()
// Unescape the URL to make it human-readable
out, err := url.QueryUnescape(u.String())
if err != nil {
return "", err
}
return out, nil
case model.DatabaseDriverMysql:
cfg, err := mysql.ParseDSN(dataSource)
if err != nil {
return "", err
}
cfg.User = "****"
cfg.Passwd = "****"
return cfg.FormatDSN(), nil
default:
return "", errors.New("invalid drivername. Not postgres or mysql.")
}
}

Просмотреть файл

@@ -6,8 +6,6 @@ package sql
import ( import (
"testing" "testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -65,53 +63,3 @@ func TestResetReadTimeout(t *testing.T) {
}) })
} }
} }
func TestSanitizeDataSource(t *testing.T) {
t.Run(model.DatabaseDriverPostgres, func(t *testing.T) {
testCases := []struct {
Original string
Sanitized string
}{
{
"",
"//****:****@",
},
{
"postgres://mmuser:mostest@localhost",
"postgres://****:****@localhost",
},
{
"postgres://mmuser:mostest@localhost/dummy?sslmode=disable",
"postgres://****:****@localhost/dummy?sslmode=disable",
},
{
"postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest",
"postgres://****:****@localhost/dummy?sslmode=disable",
},
}
driver := model.DatabaseDriverPostgres
for _, tc := range testCases {
out, err := SanitizeDataSource(driver, tc.Original)
require.NoError(t, err)
assert.Equal(t, tc.Sanitized, out)
}
})
t.Run(model.DatabaseDriverMysql, func(t *testing.T) {
testCases := []struct {
Original string
Sanitized string
}{
{
"mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s",
"****:****@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s",
},
}
driver := model.DatabaseDriverMysql
for _, tc := range testCases {
out, err := SanitizeDataSource(driver, tc.Original)
require.NoError(t, err)
assert.Equal(t, tc.Sanitized, out)
}
})
}