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
+136
View File
@@ -0,0 +1,136 @@
package store
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// Store is the single writer of the metadata document. Every mutation runs on
// a private copy of the state and is swapped in only once it has reached the
// disk, so a reader never observes a half-applied update and a crash never
// leaves a partial file.
type Store struct {
path string
mu sync.RWMutex
s *State
}
// Open loads the state document at path, creating an empty one if absent.
func Open(path string) (*Store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return nil, err
}
st := &Store{path: path, s: newState()}
b, err := os.ReadFile(path)
switch {
case os.IsNotExist(err):
return st, st.persist(st.s)
case err != nil:
return nil, err
}
s := newState()
if err := json.Unmarshal(b, s); err != nil {
return nil, fmt.Errorf("store: parse %s: %w", path, err)
}
s.init()
st.s = s
return st, nil
}
// Read runs fn against a consistent view of the state. fn must not retain or
// mutate anything it is given.
func (st *Store) Read(fn func(*State)) {
st.mu.RLock()
defer st.mu.RUnlock()
fn(st.s)
}
// Update applies fn to a copy of the state and commits it. If fn or the write
// fails, the stored state is left untouched.
func (st *Store) Update(fn func(*State) error) error {
st.mu.Lock()
defer st.mu.Unlock()
next, err := clone(st.s)
if err != nil {
return err
}
if err := fn(next); err != nil {
return err
}
if err := st.persist(next); err != nil {
return err
}
st.s = next
return nil
}
// GC drops expired sessions, device logins and invites.
func (st *Store) GC(now time.Time) error {
return st.Update(func(s *State) error {
for k, v := range s.Sessions {
if now.After(v.ExpiresAt) {
delete(s.Sessions, k)
}
}
for k, v := range s.Devices {
if now.After(v.ExpiresAt) {
delete(s.Devices, k)
}
}
for k, v := range s.Invites {
if !v.Usable(now) {
delete(s.Invites, k)
}
}
return nil
})
}
func (st *Store) persist(s *State) error {
b, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return writeFileAtomic(st.path, b, 0o600)
}
func clone(s *State) (*State, error) {
b, err := json.Marshal(s)
if err != nil {
return nil, err
}
out := newState()
if err := json.Unmarshal(b, out); err != nil {
return nil, err
}
out.init()
return out, nil
}
// writeFileAtomic replaces path with data, surviving a crash mid-write.
func writeFileAtomic(path string, data []byte, mode os.FileMode) error {
tmp := path + ".tmp"
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(tmp)
return err
}
if err := f.Sync(); err != nil {
f.Close()
os.Remove(tmp)
return err
}
if err := f.Close(); err != nil {
os.Remove(tmp)
return err
}
return os.Rename(tmp, path)
}