补齐初始化外部证书预检查并增加回归用例

This commit is contained in:
smail 2026-10-01 19:05:05 +08:00
parent b48ba0beca
commit 3c1e3cefc7
2 changed files with 70 additions and 7 deletions

View File

@ -115,6 +115,10 @@ var legacyPluginExecutableMap = map[string]string{
var legacyMySQLDSNPattern = regexp.MustCompile(`^([^:@/[:space:]]+):([^\r\n]*)@tcp\(([^():/[:space:]]+)(?::([0-9]{1,5}))?\)/([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:\?[^#[:space:]]*)?$`)
func PlanLegacyMigration(root string, _ RuntimeValidationOptions) (LegacyMigrationPlan, error) {
return planLegacyMigration(root, false)
}
func planLegacyMigration(root string, allowExternalSSL bool) (LegacyMigrationPlan, error) {
directory, err := secureManagerConfigDirectory(root)
if err != nil {
return LegacyMigrationPlan{}, err
@ -144,7 +148,7 @@ func PlanLegacyMigration(root string, _ RuntimeValidationOptions) (LegacyMigrati
if err := json.Unmarshal(data, &legacy); err != nil {
return LegacyMigrationPlan{}, err
}
plan.ManualFields = legacyManualFields(root, legacy)
plan.ManualFields = legacyManualFields(root, legacy, allowExternalSSL)
if len(plan.ManualFields) > 0 {
return plan, ErrMigrationUnsafeValue
}
@ -155,7 +159,7 @@ func MigrateLegacyConfig(root string, options RuntimeValidationOptions) (LegacyM
}
func migrateLegacyConfig(root string, options RuntimeValidationOptions, completeSetup bool) (LegacyMigrationResult, error) {
plan, err := PlanLegacyMigration(root, options)
plan, err := planLegacyMigration(root, completeSetup)
if err != nil {
return LegacyMigrationResult{}, err
}
@ -383,20 +387,25 @@ func equivalentStoredSecret(expected, existing StoredSecret, vault *credential.V
return expectedErr == nil && existingErr == nil && expectedPlaintext == existingPlaintext
}
func legacyManualFields(root string, legacy legacyMigrationConfig) []string {
func legacyManualFields(root string, legacy legacyMigrationConfig, allowExternalSSL ...bool) []string {
permitExternal := len(allowExternalSSL) > 0 && allowExternalSSL[0]
validReference := func(value string) bool {
_, ok := legacyControlledReference(value)
return ok || (permitExternal && filepath.IsAbs(strings.TrimSpace(value)))
}
fields := make([]string, 0)
_, _, databaseManualFields := legacyDatabasePlan(root, legacy)
fields = append(fields, databaseManualFields...)
if _, ok := legacyControlledReference(legacy.SSLPublicKeyPath); legacy.SSLPublicKeyPath != "" && !ok {
if legacy.SSLPublicKeyPath != "" && !validReference(legacy.SSLPublicKeyPath) {
fields = append(fields, "SSLPublicKeyPath")
}
if _, ok := legacyControlledReference(legacy.SSLPrivateKeyPath); legacy.SSLPrivateKeyPath != "" && !ok {
if legacy.SSLPrivateKeyPath != "" && !validReference(legacy.SSLPrivateKeyPath) {
fields = append(fields, "SSLPrivateKeyPath")
}
if _, ok := legacyControlledReference(legacy.SSL.CertificateRef); legacy.SSL.CertificateRef != "" && !ok {
if legacy.SSL.CertificateRef != "" && !validReference(legacy.SSL.CertificateRef) {
fields = append(fields, "ssl.certificateRef")
}
if _, ok := legacyControlledReference(legacy.SSL.PrivateKeyRef); legacy.SSL.PrivateKeyRef != "" && !ok {
if legacy.SSL.PrivateKeyRef != "" && !validReference(legacy.SSL.PrivateKeyRef) {
fields = append(fields, "ssl.privateKeyRef")
}
if legacy.SMTPUsername == "" && legacy.SMTPPassword != "" {

View File

@ -738,6 +738,60 @@ func TestMigrateLegacyConfigDerivesSetupStateFromFullPreflight(t *testing.T) {
}
}
func TestSetupMigrationAcceptsExternalSSLOnlyDuringFinalization(t *testing.T) {
for _, completing := range []bool{false, true} {
root := t.TempDir()
external := t.TempDir()
certificate := filepath.ToSlash(filepath.Join(external, "public.crt"))
privateKey := filepath.ToSlash(filepath.Join(external, "private.key"))
legacy := map[string]any{
"domain": "example.com", "webDomain": "web.example.com",
"dbDSN": "./data/smail.db", "dbType": "sqlite",
"isInit": false, "setupState": "bootstrap",
"ssl": map[string]any{
"management": SSLManagementWebServer,
"mode": ManagedSSLModeUpload, "defaultDirectory": "ssl",
"customDirectory": "ssl", "certificateRef": certificate,
"privateKeyRef": privateKey,
},
}
writeLegacyMigrationFixture(t, root, legacy)
options := RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
if _, err := PlanLegacyMigration(root, options); !errors.Is(err, ErrMigrationUnsafeValue) {
t.Fatalf("ordinary plan must reject external references: %v", err)
}
_, err := migrateLegacyConfig(root, options, completing)
if !completing {
if !errors.Is(err, ErrMigrationUnsafeValue) {
t.Fatalf("ordinary migration must reject external references: %v", err)
}
continue
}
if err != nil {
t.Fatal(err)
}
runtime, err := NewConfigLoader(root, ConfigLoaderOptions{Validation: options}).Load()
if err != nil {
t.Fatal(err)
}
if runtime.SetupState != SetupStateReady || runtime.SSL.TLS.CertificateRef != certificate || runtime.SSL.TLS.PrivateKeyRef != privateKey {
t.Fatalf("setup did not preserve external SSL references: %+v", runtime.SSL.TLS)
}
}
}
func TestSetupMigrationStillRejectsUnsafeRelativeSSL(t *testing.T) {
legacy := legacyMigrationConfig{DbType: "sqlite", DbDSN: "./data/smail.db"}
legacy.SSL.CertificateRef = "../outside/public.crt"
legacy.SSL.PrivateKeyRef = "../outside/private.key"
if fields := legacyManualFields(t.TempDir(), legacy, true); len(fields) == 0 {
t.Fatal("setup preflight accepted unsafe relative SSL paths")
}
if _, fields := legacySSLConfig(legacy, true); len(fields) == 0 {
t.Fatal("setup conversion accepted unsafe relative SSL paths")
}
}
func TestMigrateLegacyConfigAcceptsStructuredSSLObject(t *testing.T) {
root := t.TempDir()
legacy := map[string]any{