完善配置操作取消边界
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-08-30 22:19:14 +08:00
parent d060a36d57
commit 37a0381b67
4 changed files with 160 additions and 0 deletions

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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
}

View File

@ -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) {