修复PostgreSQL首次安装连接选项与配置迁移不一致
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-21 01:23:54 +08:00
parent 6fe278f612
commit fa0cea93b8
4 changed files with 67 additions and 3 deletions

View File

@ -683,9 +683,21 @@ func parseLegacyMySQLDSN(value string) (DatabaseConfig, string, bool) {
func parseLegacyPostgreSQLDSN(value string) (DatabaseConfig, string, bool) {
parsed, err := url.ParseRequestURI(value)
if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.User == nil || parsed.Host == "" {
if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") || parsed.Fragment != "" || parsed.User == nil || parsed.Host == "" {
return DatabaseConfig{}, "", false
}
// 仅接受正式运行能够原样表达的选项;未知、重复和降级 TLS 的选项仍需人工迁移。
query, err := url.ParseQuery(parsed.RawQuery)
if err != nil {
return DatabaseConfig{}, "", false
}
supported := map[string]string{"sslmode": "require", "timezone": "UTC", "search_path": "public"}
for key, values := range query {
expected, known := supported[key]
if !known || len(values) != 1 || values[0] != expected {
return DatabaseConfig{}, "", false
}
}
username := parsed.User.Username()
password, ok := parsed.User.Password()
if !ok || !validLegacyDatabaseUser(username) || !validLegacyDatabasePassword(password) {

View File

@ -0,0 +1,51 @@
package config
import "testing"
func TestLegacyPostgresRuntimeOptions(t *testing.T) {
for _, test := range []struct {
query string
valid bool
}{
{"", true},
{"sslmode=require", true},
{"sslmode=require&timezone=UTC&search_path=public", true},
{"sslmode=disable", false},
{"sslmode=prefer", false},
{"sslmode=verify-full", false},
{"sslmode=require&sslmode=disable", false},
{"sslmode=require&sslmode=require", false},
{"sslmode=require&options=-c%20search_path%3Dprivate", false},
{"sslmode=require&search_path=private", false},
{"sslmode=require&timezone=Asia%2FShanghai", false},
{"sslmode=require&unknown=", false},
{"sslmode=require;search_path=private", false},
{"sslmode=%zz", false},
} {
t.Run(test.query, func(t *testing.T) {
value := "postgres://smail:sample@127.0.0.1:5432/smail"
if test.query != "" {
value += "?" + test.query
}
database, password, valid := parseLegacyPostgreSQLDSN(value)
if valid != test.valid {
t.Fatalf("连接选项验证错误:%t", valid)
}
if valid && (database.Type != DatabaseTypePostgreSQL || database.Port != 5432 || password != "sample") {
t.Fatal("安全选项影响了数据库连接字段")
}
})
}
}
func TestLegacyPostgresSetupDSNMigration(t *testing.T) {
root := t.TempDir()
writeLegacyMigrationFixture(t, root, map[string]any{
"dbType": DBTypePostgres,
"dbDSN": "postgres://smail:sample@127.0.0.1:5432/smail?sslmode=require",
})
plan, err := PlanLegacyMigration(root, RuntimeValidationOptions{})
if err != nil || len(plan.ManualFields) != 0 {
t.Fatalf("首次安装产生的安全连接串不能迁移:%v", err)
}
}

View File

@ -363,7 +363,8 @@ func buildPostgresDSN(settings DatabaseSettings, previousDSN string) (string, er
query = previous.Query()
}
if query.Get("sslmode") == "" {
query.Set("sslmode", "disable")
// 首次安装与正式运行保持相同的 TLS 要求,避免完成安装时切换连接语义。
query.Set("sslmode", "require")
}
parsed := &url.URL{
Scheme: "postgres",

View File

@ -135,7 +135,7 @@ func TestBuildPostgresDSNUsesStructuredFields(t *testing.T) {
if host != "2001:db8::2" || port != "5432" || strings.TrimPrefix(parsed.Path, "/") != "smail_1" {
t.Fatalf("unexpected connection settings: host=%q port=%q name=%q", host, port, parsed.Path)
}
if parsed.Query().Get("sslmode") != "disable" {
if parsed.Query().Get("sslmode") != "require" {
t.Fatalf("unexpected sslmode: %q", parsed.Query().Get("sslmode"))
}
}