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
+143
View File
@@ -0,0 +1,143 @@
package store
import "sort"
// maxDepth bounds every tree walk so a corrupted parent cycle cannot hang the
// server. Group creation rejects cycles, so this is a backstop only.
const maxDepth = 64
// Ancestors returns groupID and every parent above it, nearest first.
func (s *State) Ancestors(groupID string) []string {
out := []string{}
seen := map[string]bool{}
for id := groupID; id != "" && !seen[id] && len(out) < maxDepth; {
g, ok := s.Groups[id]
if !ok {
break
}
seen[id] = true
out = append(out, id)
id = g.ParentID
}
return out
}
// Descendants returns groupID and every group below it.
func (s *State) Descendants(groupID string) []string {
if _, ok := s.Groups[groupID]; !ok {
return nil
}
out := []string{groupID}
for i := 0; i < len(out); i++ {
for id, g := range s.Groups {
if g.ParentID == out[i] && !contains(out, id) {
out = append(out, id)
}
}
}
sort.Strings(out)
return out
}
// IsAncestor reports whether anc is at or above desc in the tree.
func (s *State) IsAncestor(anc, desc string) bool {
return contains(s.Ancestors(desc), anc)
}
// EffectiveGroups is the set of groups whose grants reach the user: every
// group the user belongs to, plus every group above those. A grant on
// "Company" therefore reaches a member of "Company/Engineering/Backend".
func (s *State) EffectiveGroups(userID string) map[string]bool {
out := map[string]bool{}
for _, m := range s.Memberships {
if m.UserID != userID {
continue
}
for _, id := range s.Ancestors(m.GroupID) {
out[id] = true
}
}
return out
}
// IsGroupAdmin reports whether the user administers groupID, either directly
// or by administering a group above it. Server admins administer everything.
func (s *State) IsGroupAdmin(userID, groupID string) bool {
if u := s.Users[userID]; u != nil && u.ServerAdmin {
return true
}
anc := s.Ancestors(groupID)
for _, m := range s.Memberships {
if m.UserID == userID && m.Role == RoleAdmin && contains(anc, m.GroupID) {
return true
}
}
return false
}
// DatabasePerm is the strongest permission the user holds on a database.
func (s *State) DatabasePerm(userID, dbID string) Perm {
db, ok := s.Databases[dbID]
if !ok {
return PermNone
}
if s.IsGroupAdmin(userID, db.OwnerGroupID) {
return PermAdmin
}
reach := s.EffectiveGroups(userID)
best := PermNone
for _, g := range s.Grants {
if g.DatabaseID != dbID || !reach[g.GroupID] {
continue
}
if g.Perm.Rank() > best.Rank() {
best = g.Perm
}
}
return best
}
// Assignment pairs a database with the caller's permission on it.
type Assignment struct {
DB *Database
Perm Perm
}
// Assignments lists every database the user may see, strongest permission
// first resolved, sorted by name for a stable client view.
func (s *State) Assignments(userID string) []Assignment {
out := []Assignment{}
for id, db := range s.Databases {
if p := s.DatabasePerm(userID, id); p != PermNone {
out = append(out, Assignment{DB: db, Perm: p})
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].DB.Name != out[j].DB.Name {
return out[i].DB.Name < out[j].DB.Name
}
return out[i].DB.ID < out[j].DB.ID
})
return out
}
// Watchers lists the user IDs that must be told when a database changes.
func (s *State) Watchers(dbID string) []string {
out := []string{}
for id := range s.Users {
if s.DatabasePerm(id, dbID) != PermNone {
out = append(out, id)
}
}
sort.Strings(out)
return out
}
func contains(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}