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.
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
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")
|
|
}
|
|
}
|