From 37a0381b673f65b30f150f7cb03cf4406c8f75de Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 30 Aug 2026 22:19:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=85=8D=E7=BD=AE=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=8F=96=E6=B6=88=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/database_migration.go | 9 +++ server/config/database_migration_test.go | 17 +++++ server/config/manager_v2.go | 48 +++++++++++++ server/config/manager_v2_test.go | 86 ++++++++++++++++++++++++ 4 files changed, 160 insertions(+) diff --git a/server/config/database_migration.go b/server/config/database_migration.go index a41b50e..a995bfc 100644 --- a/server/config/database_migration.go +++ b/server/config/database_migration.go @@ -24,6 +24,9 @@ func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration u if err := m.authorize(ctx, actor, ConfigOperationMigration, SectionRoot); err != nil { return err } + if err := contextError(ctx); err != nil { + return err + } m.writeMu.Lock() defer m.writeMu.Unlock() @@ -62,9 +65,15 @@ func (m *ConfigManager) SwitchDatabase(ctx context.Context, expectedGeneration u if err := m.snapshots.Validate(candidate); err != nil { return err } + if err := contextError(ctx); err != nil { + return err + } if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil { return err } + if err := contextError(ctx); err != nil { + return err + } if err := m.transactions.Commit(current, candidate); err != nil { return err } diff --git a/server/config/database_migration_test.go b/server/config/database_migration_test.go index 76315f1..1f1ff53 100644 --- a/server/config/database_migration_test.go +++ b/server/config/database_migration_test.go @@ -69,3 +69,20 @@ func TestConfigManagerSwitchDatabaseRequiresAuthorizedActor(t *testing.T) { t.Fatalf("missing actor error = %v, want authorization required", err) } } + +func TestConfigManagerSwitchDatabaseHonorsCancellationBeforePersistence(t *testing.T) { + root := t.TempDir() + writeRuntimeFixture(t, root, validRuntimeConfig()) + manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}) + if err != nil { + t.Fatal(err) + } + target := manager.Snapshot().Root.Database + target.LocalRef = "next.db" + if err := manager.SwitchDatabase(newCancelAfterFirstContext(), 1, target, authorizedConfigActor()); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled database switch error = %v, want context canceled", err) + } + if manager.Snapshot().Generation != 1 || manager.Snapshot().Root.Database.LocalRef != "smail.db" { + t.Fatalf("canceled database switch changed snapshot: %+v", manager.Snapshot()) + } +} diff --git a/server/config/manager_v2.go b/server/config/manager_v2.go index 188708a..6e2ea1f 100644 --- a/server/config/manager_v2.go +++ b/server/config/manager_v2.go @@ -301,10 +301,16 @@ func (m *ConfigManager) Update(ctx context.Context, req UpdateRequest) (*UpdateR if err := m.authorize(ctx, req.Actor, ConfigOperationUpdate, req.Section); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } candidate, err := applySectionPayload(current, req.Section, req.Payload, req.SecretChanges, m.vault) if err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } setCandidateGeneration(&candidate, current.Generation+1) if err := ValidateRuntimeConfig(candidate, m.options); err != nil { return nil, err @@ -315,6 +321,9 @@ func (m *ConfigManager) Update(ctx context.Context, req UpdateRequest) (*UpdateR if err := m.confirm(ctx, req.Actor, req.Confirmation, ConfigOperationUpdate, req.Section); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } m.mu.Lock() m.pending = &pendingConfig{Candidate: candidate, Section: req.Section, Actor: req.Actor, Confirmation: req.Confirmation} m.status.PendingRestart = servicesForSection(req.Section) @@ -352,6 +361,9 @@ func (m *ConfigManager) apply(ctx context.Context, generation uint64, actor Audi } m.mu.RUnlock() candidate := pending.Candidate + if err := contextError(ctx); err != nil { + return nil, err + } if actor.UserID != "" && (pending.Actor.UserID != actor.UserID || pending.Actor.SessionID != actor.SessionID) { return nil, ErrAuthorizationRequired } @@ -370,9 +382,15 @@ func (m *ConfigManager) apply(ctx context.Context, generation uint64, actor Audi if err := m.snapshots.Validate(candidate); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if err := m.transactions.Commit(current, candidate); err != nil { return nil, err } @@ -436,6 +454,9 @@ func (m *ConfigManager) PreviewRollback(ctx context.Context, generation uint64, if err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if err := ValidateRuntimeSecrets(m.vault, backup); err != nil { return nil, err } @@ -471,6 +492,9 @@ func (m *ConfigManager) IssueRollbackTicket(ctx context.Context, generation uint if err != nil { return "", err } + if err := contextError(ctx); err != nil { + return "", err + } if err := ValidateRuntimeSecrets(m.vault, backup); err != nil { return "", err } @@ -523,6 +547,9 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A return nil, err } } + if err := contextError(ctx); err != nil { + return nil, err + } m.writeMu.Lock() defer m.writeMu.Unlock() m.mu.RLock() @@ -536,6 +563,9 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A if err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if err := ValidateRuntimeSecrets(m.vault, candidate); err != nil { return nil, err } @@ -547,9 +577,15 @@ func (m *ConfigManager) rollback(ctx context.Context, generation uint64, actor A if err := m.snapshots.Validate(candidate); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil { return nil, err } + if err := contextError(ctx); err != nil { + return nil, err + } if err := m.transactions.Commit(current, candidate); err != nil { return nil, err } @@ -826,6 +862,9 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer values := make([]LegacyPluginSecret, 0, len(current.Plugins.Plugins)) for logicalID, instance := range current.Plugins.Plugins { + if err := contextError(ctx); err != nil { + return false, err + } if !storedSecretHasMaterial(instance.Secret) { continue } @@ -845,6 +884,9 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer if err := importer(ctx, values); err != nil { return false, err } + if err := contextError(ctx); err != nil { + return false, err + } candidate := cloneRuntimeConfig(current) for logicalID, instance := range candidate.Plugins.Plugins { @@ -858,9 +900,15 @@ func (m *ConfigManager) MigrateLegacyPluginSecrets(ctx context.Context, importer if err := m.snapshots.Validate(candidate); err != nil { return false, err } + if err := contextError(ctx); err != nil { + return false, err + } if _, _, err := m.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil { return false, err } + if err := contextError(ctx); err != nil { + return false, err + } if err := m.transactions.Commit(current, candidate); err != nil { return false, err } diff --git a/server/config/manager_v2_test.go b/server/config/manager_v2_test.go index 88064bc..39c32f5 100644 --- a/server/config/manager_v2_test.go +++ b/server/config/manager_v2_test.go @@ -247,6 +247,60 @@ func TestConfigManagerLoadHonorsCancellationAfterReadingConfiguration(t *testing } } +func TestConfigManagerUpdateHonorsCancellationBeforeCreatingPendingState(t *testing.T) { + root := t.TempDir() + writeRuntimeFixture(t, root, validRuntimeConfig()) + manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}) + if err != nil { + t.Fatal(err) + } + payload, err := json.Marshal(manager.Snapshot().Email) + if err != nil { + t.Fatal(err) + } + if _, err := manager.Update(newCancelAfterFirstContext(), UpdateRequest{ + Section: SectionEmail, ExpectedGeneration: 1, Payload: payload, Actor: authorizedConfigActor(), + }); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled update error = %v, want context canceled", err) + } + status := manager.Status() + if len(status.PendingRestart) != 0 { + t.Fatalf("canceled update left pending restart state: %+v", status.PendingRestart) + } + if manager.Snapshot().Generation != 1 { + t.Fatalf("canceled update changed active generation: %d", manager.Snapshot().Generation) + } +} + +func TestConfigManagerApplyHonorsCancellationBeforePersistence(t *testing.T) { + root := t.TempDir() + writeRuntimeFixture(t, root, validRuntimeConfig()) + manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}) + if err != nil { + t.Fatal(err) + } + payload, err := json.Marshal(manager.Snapshot().Email) + if err != nil { + t.Fatal(err) + } + updated, err := manager.Update(context.Background(), UpdateRequest{ + Section: SectionEmail, ExpectedGeneration: 1, Payload: payload, Actor: authorizedConfigActor(), + }) + if err != nil { + t.Fatal(err) + } + if _, err := manager.Apply(newCancelAfterFirstContext(), updated.Generation); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled apply error = %v, want context canceled", err) + } + if manager.Snapshot().Generation != 1 { + t.Fatalf("canceled apply changed active generation: %d", manager.Snapshot().Generation) + } + status := manager.Status() + if len(status.PendingRestart) != 1 || status.PendingRestart[0] != ServiceEmail { + t.Fatalf("canceled apply lost pending state: %+v", status.PendingRestart) + } +} + func TestConfigManagerApplyPreflightsSnapshotBeforePersisting(t *testing.T) { root := t.TempDir() writeRuntimeFixture(t, root, validRuntimeConfig()) @@ -847,6 +901,38 @@ func TestConfigManagerRetainsLegacySecretWhenImportFails(t *testing.T) { } } +func TestConfigManagerLegacySecretMigrationHonorsCancellationAfterImport(t *testing.T) { + root := t.TempDir() + runtime := validRuntimeConfig() + vault, err := credential.NewVault(root) + if err != nil { + t.Fatal(err) + } + stored, err := ApplySecretChange(vault, StoredSecret{}, SecretChange{Operation: SecretReplace, Value: &SecretInput{Plaintext: "legacy-token"}}, SecretPolicy{Required: true, MaxLength: 4096}, time.Now().UTC()) + if err != nil { + t.Fatal(err) + } + plugin := runtime.Plugins.Plugins["telegram_push"] + plugin.Secret = stored + runtime.Plugins.Plugins["telegram_push"] = plugin + writeRuntimeFixture(t, root, runtime) + manager, err := NewConfigManager(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if migrated, err := manager.MigrateLegacyPluginSecrets(ctx, func(context.Context, []LegacyPluginSecret) error { + cancel() + return nil + }); !errors.Is(err, context.Canceled) || migrated { + t.Fatalf("canceled legacy migration = %v, %v", migrated, err) + } + if manager.Snapshot().Generation != 1 || !manager.Snapshot().Plugins.Plugins["telegram_push"].Secret.Configured() { + t.Fatal("canceled legacy migration altered the active configuration") + } +} + func containsAny(value string, terms ...string) bool { for _, term := range terms { if strings.Contains(value, term) {