mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
fix: require DSNs for legacy database notifications
Reject pre-KV PostgreSQL and MySQL targets that lack a canonical connection string, propagate the typed migration error to the fatal startup boundary, and stop emitting unregistered discrete connection keys.\n\nCloses the implementation for #53; release and issue closure remain separate gates.
This commit is contained in:
@@ -26,6 +26,25 @@ import (
|
||||
"github.com/minio/minio/internal/event/target"
|
||||
)
|
||||
|
||||
// LegacyDatabaseTargetError reports a pre-KV database notification target
|
||||
// that cannot be migrated safely. It deliberately carries no configuration
|
||||
// values so credentials cannot escape through startup logs.
|
||||
type LegacyDatabaseTargetError struct {
|
||||
subsystem string
|
||||
target string
|
||||
connectionKey string
|
||||
invalid bool
|
||||
}
|
||||
|
||||
func (e *LegacyDatabaseTargetError) Error() string {
|
||||
if e.invalid {
|
||||
return fmt.Sprintf("%s:%s has invalid %s or target settings; fix the target before migrating to SILO",
|
||||
e.subsystem, e.target, e.connectionKey)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s requires %s; discrete database connection fields are not migrated to SILO",
|
||||
e.subsystem, e.target, e.connectionKey)
|
||||
}
|
||||
|
||||
// SetNotifyKafka - helper for config migration from older config.
|
||||
func SetNotifyKafka(s config.Config, name string, cfg target.KafkaArgs) error {
|
||||
if !cfg.Enable {
|
||||
@@ -325,8 +344,21 @@ func SetNotifyPostgres(s config.Config, psqName string, cfg target.PostgreSQLArg
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.ConnectionString == "" {
|
||||
return &LegacyDatabaseTargetError{
|
||||
subsystem: config.NotifyPostgresSubSys,
|
||||
target: psqName,
|
||||
connectionKey: target.PostgresConnectionString,
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
return &LegacyDatabaseTargetError{
|
||||
subsystem: config.NotifyPostgresSubSys,
|
||||
target: psqName,
|
||||
connectionKey: target.PostgresConnectionString,
|
||||
invalid: true,
|
||||
}
|
||||
}
|
||||
|
||||
s[config.NotifyPostgresSubSys][psqName] = config.KVS{
|
||||
@@ -346,26 +378,6 @@ func SetNotifyPostgres(s config.Config, psqName string, cfg target.PostgreSQLArg
|
||||
Key: target.PostgresTable,
|
||||
Value: cfg.Table,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresHost,
|
||||
Value: cfg.Host.String(),
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresPort,
|
||||
Value: cfg.Port,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresUsername,
|
||||
Value: cfg.Username,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresPassword,
|
||||
Value: cfg.Password,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresDatabase,
|
||||
Value: cfg.Database,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.PostgresQueueDir,
|
||||
Value: cfg.QueueDir,
|
||||
@@ -538,8 +550,21 @@ func SetNotifyMySQL(s config.Config, sqlName string, cfg target.MySQLArgs) error
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.DSN == "" {
|
||||
return &LegacyDatabaseTargetError{
|
||||
subsystem: config.NotifyMySQLSubSys,
|
||||
target: sqlName,
|
||||
connectionKey: target.MySQLDSNString,
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
return &LegacyDatabaseTargetError{
|
||||
subsystem: config.NotifyMySQLSubSys,
|
||||
target: sqlName,
|
||||
connectionKey: target.MySQLDSNString,
|
||||
invalid: true,
|
||||
}
|
||||
}
|
||||
|
||||
s[config.NotifyMySQLSubSys][sqlName] = config.KVS{
|
||||
@@ -559,26 +584,6 @@ func SetNotifyMySQL(s config.Config, sqlName string, cfg target.MySQLArgs) error
|
||||
Key: target.MySQLTable,
|
||||
Value: cfg.Table,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLHost,
|
||||
Value: cfg.Host.String(),
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLPort,
|
||||
Value: cfg.Port,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLUsername,
|
||||
Value: cfg.User,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLPassword,
|
||||
Value: cfg.Password,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLDatabase,
|
||||
Value: cfg.Database,
|
||||
},
|
||||
config.KV{
|
||||
Key: target.MySQLQueueDir,
|
||||
Value: cfg.QueueDir,
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/internal/config"
|
||||
@@ -26,6 +28,25 @@ import (
|
||||
"github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
func assertLegacyDatabaseTargetError(t *testing.T, err error, subsystem, name, key string, secrets ...string) {
|
||||
t.Helper()
|
||||
var targetErr *LegacyDatabaseTargetError
|
||||
if !errors.As(err, &targetErr) {
|
||||
t.Fatalf("error = %v, want *LegacyDatabaseTargetError", err)
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{subsystem + config.SubSystemSeparator + name, key} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Errorf("error %q does not contain %q", msg, want)
|
||||
}
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if secret != "" && strings.Contains(msg, secret) {
|
||||
t.Errorf("error leaks configuration value %q: %s", secret, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// T5 (NATS): a config produced by the legacy migration must survive validation
|
||||
// and round-trip back through the parser unchanged. Before the fix the
|
||||
// migration wrote an env var name as a config key, so every migrated NATS
|
||||
@@ -113,3 +134,172 @@ func TestSetNotifyAMQPRoundTrip(t *testing.T) {
|
||||
t.Errorf("Internal = true, want false (immediate must not be written to the internal key)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNotifyDatabaseTargetsRequireConnectionStrings(t *testing.T) {
|
||||
postgresHost, err := xnet.ParseHost("legacy-postgres.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mysqlHost, err := xnet.ParseURL("legacy-mysql.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subsystem string
|
||||
key string
|
||||
set func(config.Config) error
|
||||
secrets []string
|
||||
}{
|
||||
{
|
||||
name: "postgres",
|
||||
subsystem: config.NotifyPostgresSubSys,
|
||||
key: target.PostgresConnectionString,
|
||||
set: func(s config.Config) error {
|
||||
return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{
|
||||
Enable: true,
|
||||
Format: formatNamespace,
|
||||
Table: "events",
|
||||
Host: *postgresHost,
|
||||
Port: "5432",
|
||||
Username: "legacy-user",
|
||||
Password: "legacy-postgres-password",
|
||||
Database: "legacy-database",
|
||||
})
|
||||
},
|
||||
secrets: []string{postgresHost.String(), "5432", "legacy-user", "legacy-postgres-password", "legacy-database"},
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
subsystem: config.NotifyMySQLSubSys,
|
||||
key: target.MySQLDSNString,
|
||||
set: func(s config.Config) error {
|
||||
return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{
|
||||
Enable: true,
|
||||
Format: formatNamespace,
|
||||
Table: "events",
|
||||
Host: *mysqlHost,
|
||||
Port: "3306",
|
||||
User: "legacy-user",
|
||||
Password: "legacy-mysql-password",
|
||||
Database: "legacy-database",
|
||||
})
|
||||
},
|
||||
secrets: []string{mysqlHost.String(), "3306", "legacy-user", "legacy-mysql-password", "legacy-database"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := config.Config{test.subsystem: map[string]config.KVS{}}
|
||||
err := test.set(s)
|
||||
assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secrets...)
|
||||
if _, ok := s[test.subsystem][testTargetName]; ok {
|
||||
t.Fatal("unsupported target was emitted despite migration error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNotifyDisabledDatabaseTargetsAreIgnored(t *testing.T) {
|
||||
s := config.Config{
|
||||
config.NotifyPostgresSubSys: map[string]config.KVS{},
|
||||
config.NotifyMySQLSubSys: map[string]config.KVS{},
|
||||
}
|
||||
if err := SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{Password: "discarded-postgres-secret"}); err != nil {
|
||||
t.Fatalf("SetNotifyPostgres: %v", err)
|
||||
}
|
||||
if err := SetNotifyMySQL(s, testTargetName, target.MySQLArgs{Password: "discarded-mysql-secret"}); err != nil {
|
||||
t.Fatalf("SetNotifyMySQL: %v", err)
|
||||
}
|
||||
if _, ok := s[config.NotifyPostgresSubSys][testTargetName]; ok {
|
||||
t.Fatal("disabled Postgres target was emitted")
|
||||
}
|
||||
if _, ok := s[config.NotifyMySQLSubSys][testTargetName]; ok {
|
||||
t.Fatal("disabled MySQL target was emitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNotifyInvalidDatabaseTargetsDoNotLeak(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subsystem string
|
||||
key string
|
||||
secret string
|
||||
set func(config.Config) error
|
||||
}{
|
||||
{
|
||||
name: "postgres",
|
||||
subsystem: config.NotifyPostgresSubSys,
|
||||
key: target.PostgresConnectionString,
|
||||
secret: "postgres-dsn-secret",
|
||||
set: func(s config.Config) error {
|
||||
return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{
|
||||
Enable: true,
|
||||
Format: formatNamespace,
|
||||
ConnectionString: "host=db password=postgres-dsn-secret",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
subsystem: config.NotifyMySQLSubSys,
|
||||
key: target.MySQLDSNString,
|
||||
secret: "mysql-dsn-secret",
|
||||
set: func(s config.Config) error {
|
||||
return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{
|
||||
Enable: true,
|
||||
Format: formatNamespace,
|
||||
DSN: "user:mysql-dsn-secret@tcp(db:3306/events",
|
||||
Table: "events",
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := config.Config{test.subsystem: map[string]config.KVS{}}
|
||||
err := test.set(s)
|
||||
assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secret)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseConnectionStringsSurviveKVTokenization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subsystem string
|
||||
key string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "postgres",
|
||||
subsystem: config.NotifyPostgresSubSys,
|
||||
key: target.PostgresConnectionString,
|
||||
input: `notify_postgres:dsn connection_string="host=db port=5432 dbname=events user=app password=inside" table="events"`,
|
||||
want: "host=db port=5432 dbname=events user=app password=inside",
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
subsystem: config.NotifyMySQLSubSys,
|
||||
key: target.MySQLDSNString,
|
||||
input: `notify_mysql:dsn dsn_string="user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside" table="events"`,
|
||||
want: "user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := config.Config{test.subsystem: map[string]config.KVS{}}
|
||||
if _, err := s.SetKVS(test.input, DefaultNotificationKVS); err != nil {
|
||||
t.Fatalf("SetKVS: %v", err)
|
||||
}
|
||||
if got := s[test.subsystem]["dsn"].Get(test.key); got != test.want {
|
||||
t.Errorf("%s = %q, want %q", test.key, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,21 +414,9 @@ var configPkgConsts = map[string]string{
|
||||
"Comment": config.Comment,
|
||||
}
|
||||
|
||||
// knownUnregisteredWrites records pre-existing instances of the exact defect
|
||||
// this audit exists to catch: a legacy migration writing config keys that no
|
||||
// default KVS registers, so the migrated config is rejected on the next load.
|
||||
//
|
||||
// These are inherited from upstream and are the same class as issue #39, but
|
||||
// they are NOT part of the issue #39 fix and were left untouched deliberately.
|
||||
// The Postgres/MySQL keys below are the pre-connection-string DSN fields; the
|
||||
// migration still writes them and `password` carries a plaintext database
|
||||
// password.
|
||||
//
|
||||
// This list must only ever shrink. Do not add entries to silence a new gap.
|
||||
var knownUnregisteredWrites = map[string][]string{
|
||||
"SetNotifyPostgres": {"host", "port", "username", "password", "database"},
|
||||
"SetNotifyMySQL": {"host", "port", "username", "password", "database"},
|
||||
}
|
||||
// knownUnregisteredWrites is a shrink-only ratchet for inherited migration
|
||||
// gaps. Do not add entries to silence a new mismatch.
|
||||
var knownUnregisteredWrites = map[string][]string{}
|
||||
|
||||
func TestNotifyConfigKeysAreRegistered(t *testing.T) {
|
||||
targetConsts, err := parseTargetPkgStringConsts("../../event/target")
|
||||
|
||||
Reference in New Issue
Block a user