Add KeepOrg sync server: accounts, nested groups, versioned KDBX sync

Why: collaborative KeePass use needs one arbiter of "who wrote last" that
never sees a master password, plus access control that follows an org chart.

What: stdlib-only Go server. Metadata is a single JSON document with one
writer and atomic commit; KDBX payloads are versioned blobs on disk. Access
comes from a group tree where a grant on a group reaches every member below
it. Login is password+TOTP, invite links, or OIDC through a browser-approved
device flow. Pushes carry If-Match and are arbitrated by a per-database merge
strategy (server-wins by default). Updates are pushed to clients over SSE.

Proved: go test ./internal/... covers RFC 6238 vectors, ACL inheritance and
isolation, store rollback, blob pruning, every push verdict, and an
end-to-end HTTP pass over login, conflict, force, read-only grants, invite
redemption, TOTP enrolment, device login and the event stream.
This commit is contained in:
2026-10-29 15:54:10 +01:00
commit 8bbb3179a1
26 changed files with 3897 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package store
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
)
func TestUpdatePersistsAndSurvivesReopen(t *testing.T) {
p := filepath.Join(t.TempDir(), "state.json")
st, err := Open(p)
if err != nil {
t.Fatal(err)
}
if err := st.Update(func(s *State) error {
s.Users["u1"] = &User{ID: "u1", Username: "alice"}
return nil
}); err != nil {
t.Fatal(err)
}
again, err := Open(p)
if err != nil {
t.Fatal(err)
}
var name string
again.Read(func(s *State) { name = s.Users["u1"].Username })
if name != "alice" {
t.Fatalf("reopened username = %q", name)
}
if fi, _ := os.Stat(p); fi.Mode().Perm() != 0o600 {
t.Fatalf("state file mode = %v", fi.Mode().Perm())
}
}
func TestFailedUpdateLeavesStateUntouched(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "state.json"))
if err != nil {
t.Fatal(err)
}
boom := errors.New("boom")
if err := st.Update(func(s *State) error {
s.Users["u1"] = &User{ID: "u1"}
return boom
}); !errors.Is(err, boom) {
t.Fatalf("err = %v", err)
}
var n int
st.Read(func(s *State) { n = len(s.Users) })
if n != 0 {
t.Fatalf("users after failed update = %d", n)
}
}
func TestGCDropsExpired(t *testing.T) {
st, _ := Open(filepath.Join(t.TempDir(), "state.json"))
now := time.Now()
_ = st.Update(func(s *State) error {
s.Sessions["old"] = &Session{Token: "old", ExpiresAt: now.Add(-time.Hour)}
s.Sessions["new"] = &Session{Token: "new", ExpiresAt: now.Add(time.Hour)}
s.Invites["spent"] = &Invite{Token: "spent", ExpiresAt: now.Add(time.Hour), MaxUses: 1, Uses: 1}
return nil
})
if err := st.GC(now); err != nil {
t.Fatal(err)
}
st.Read(func(s *State) {
if len(s.Sessions) != 1 || s.Sessions["new"] == nil {
t.Fatalf("sessions after GC = %v", s.Sessions)
}
if len(s.Invites) != 0 {
t.Fatalf("invites after GC = %v", s.Invites)
}
})
}