fix: secure profile persistence ownership

This commit is contained in:
proxyctl coordinator
2026-07-17 02:29:58 +08:00
parent 80ab995e2e
commit 1fa7e0fb2d
6 changed files with 457 additions and 66 deletions
+13
View File
@@ -77,6 +77,19 @@ func (a *Application) Dispatch(intent Intent) Result {
})
if err == nil {
a.state = next
} else {
var committed *profile.CommitError
if errors.As(err, &committed) {
// Rename committed next before directory durability reporting failed.
// Prefer that state even if the required verification reload also fails.
a.state = next
reloaded, loadErr := a.store.Load()
if loadErr != nil {
err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr))
} else {
a.state = reloaded
}
}
}
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err}
}
+41
View File
@@ -1,6 +1,7 @@
package app_test
import (
"errors"
"io"
"os"
"path/filepath"
@@ -71,6 +72,46 @@ func TestProfileConfigurationUsesIsolatedXDGPathOwnerOnlyAndAtomicReplacement(t
}
}
type committedErrorStore struct {
state profile.State
loadCalls int
}
func (s *committedErrorStore) Load() (profile.State, error) {
s.loadCalls++
return s.state, nil
}
func (s *committedErrorStore) Mutate(change func(profile.State) (profile.State, error)) (profile.State, error) {
next, err := change(s.state)
if err != nil {
return s.state, err
}
s.state = next
return next, &profile.CommitError{Err: errors.New("injected directory sync failure")}
}
func TestApplicationReloadsStateAfterCommittedDurabilityError(t *testing.T) {
store := &committedErrorStore{}
application, err := app.New(store)
if err != nil {
t.Fatal(err)
}
result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{
Name: "committed", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080",
}})
var committed *profile.CommitError
if !errors.As(result.Err, &committed) {
t.Fatalf("mutation error = %v, want committed durability error", result.Err)
}
if store.loadCalls != 2 {
t.Fatalf("Load calls = %d, want startup plus post-commit reload", store.loadCalls)
}
if len(result.Snapshot.Profiles) != 1 || result.Snapshot.Profiles[0].Name != "committed" {
t.Fatalf("Application snapshot after committed error = %#v", result.Snapshot)
}
}
func TestConcurrentApplicationsMutateLatestStateWithoutLostUpdates(t *testing.T) {
path := filepath.Join(t.TempDir(), "proxyctl", "profiles.json")
first, err := app.New(profile.NewFileStore(path))