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:
@@ -0,0 +1,90 @@
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
// tree: company > engineering > backend, company > sales
|
||||
func aclFixture() *State {
|
||||
s := newState()
|
||||
s.Groups["company"] = &Group{ID: "company", Name: "Company"}
|
||||
s.Groups["eng"] = &Group{ID: "eng", Name: "Engineering", ParentID: "company"}
|
||||
s.Groups["backend"] = &Group{ID: "backend", Name: "Backend", ParentID: "eng"}
|
||||
s.Groups["sales"] = &Group{ID: "sales", Name: "Sales", ParentID: "company"}
|
||||
s.Users["alice"] = &User{ID: "alice", Username: "alice"}
|
||||
s.Users["bob"] = &User{ID: "bob", Username: "bob"}
|
||||
s.Users["root"] = &User{ID: "root", Username: "root", ServerAdmin: true}
|
||||
s.Memberships = []Membership{
|
||||
{UserID: "alice", GroupID: "backend", Role: RoleMember},
|
||||
{UserID: "bob", GroupID: "sales", Role: RoleAdmin},
|
||||
}
|
||||
s.Databases["shared"] = &Database{ID: "shared", Name: "Shared", OwnerGroupID: "company"}
|
||||
s.Databases["salesdb"] = &Database{ID: "salesdb", Name: "Sales", OwnerGroupID: "sales"}
|
||||
s.Grants = []Grant{
|
||||
{GroupID: "company", DatabaseID: "shared", Perm: PermRead},
|
||||
{GroupID: "eng", DatabaseID: "shared", Perm: PermWrite},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func TestGrantOnAncestorReachesDescendantMember(t *testing.T) {
|
||||
s := aclFixture()
|
||||
if got := s.DatabasePerm("alice", "shared"); got != PermWrite {
|
||||
t.Fatalf("alice on shared = %q, want write (read from company, write from eng)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantDoesNotLeakAcrossSiblings(t *testing.T) {
|
||||
s := aclFixture()
|
||||
if got := s.DatabasePerm("alice", "salesdb"); got != PermNone {
|
||||
t.Fatalf("alice on salesdb = %q, want none", got)
|
||||
}
|
||||
if got := s.DatabasePerm("bob", "shared"); got != PermRead {
|
||||
t.Fatalf("bob on shared = %q, want read", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerGroupAdminGetsAdmin(t *testing.T) {
|
||||
s := aclFixture()
|
||||
if got := s.DatabasePerm("bob", "salesdb"); got != PermAdmin {
|
||||
t.Fatalf("bob on own group db = %q, want admin", got)
|
||||
}
|
||||
if !s.IsGroupAdmin("root", "backend") {
|
||||
t.Fatal("server admin should administer any group")
|
||||
}
|
||||
if s.IsGroupAdmin("bob", "eng") {
|
||||
t.Fatal("sales admin must not administer engineering")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOfParentAdministersChild(t *testing.T) {
|
||||
s := aclFixture()
|
||||
s.Memberships = append(s.Memberships, Membership{UserID: "alice", GroupID: "company", Role: RoleAdmin})
|
||||
if !s.IsGroupAdmin("alice", "backend") {
|
||||
t.Fatal("company admin should administer backend")
|
||||
}
|
||||
if got := s.DatabasePerm("alice", "salesdb"); got != PermAdmin {
|
||||
t.Fatalf("company admin on salesdb = %q, want admin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignmentsAndWatchers(t *testing.T) {
|
||||
s := aclFixture()
|
||||
got := s.Assignments("alice")
|
||||
if len(got) != 1 || got[0].DB.ID != "shared" || got[0].Perm != PermWrite {
|
||||
t.Fatalf("alice assignments = %+v", got)
|
||||
}
|
||||
w := s.Watchers("shared")
|
||||
if len(w) != 3 {
|
||||
t.Fatalf("watchers of shared = %v, want alice, bob and root", w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeWalksSurviveCycle(t *testing.T) {
|
||||
s := aclFixture()
|
||||
s.Groups["company"].ParentID = "backend" // corrupt
|
||||
if len(s.Ancestors("backend")) != 3 {
|
||||
t.Fatalf("cycle walk = %v", s.Ancestors("backend"))
|
||||
}
|
||||
if len(s.Descendants("company")) == 0 {
|
||||
t.Fatal("descendants must terminate on a cycle")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user