package access import ( "os" "path/filepath" "sync" "testing" ) // spec is a small helper to build a GroupSpec. func spec(name, parent string, members map[string]Role) GroupSpec { return GroupSpec{Name: name, Parent: parent, Members: members} } func TestUnconfiguredIsOpen(t *testing.T) { // No roles assigned anywhere → fully open (everyone is admin), matching a // fresh/dev deployment. p := New("", nil) for _, u := range []string{"", "alice", "carol"} { if !p.CanAdmin(u) { t.Errorf("unconfigured: CanAdmin(%q) = false, want true", u) } if p.Level(u) != LevelWrite { t.Errorf("unconfigured: Level(%q) = %v, want write", u, p.Level(u)) } } } func TestRoleLadder(t *testing.T) { p := New("", []GroupSpec{ spec("public", "", map[string]Role{"adm": RoleAdmin}), spec("ops", "", map[string]Role{ "viewer": RoleViewer, "op": RoleOperator, "logic": RoleLogic, "aud": RoleAuditor, }), }) // Configured now → unlisted users (and anonymous) are viewers (read-only). if p.Level("") != LevelRead { t.Errorf("anonymous Level = %v, want read", p.Level("")) } if p.Level("nobody") != LevelRead { t.Errorf("unlisted Level = %v, want read", p.Level("nobody")) } cases := []struct { user string level Level editLogic bool audit bool admin bool }{ {"viewer", LevelRead, false, false, false}, {"op", LevelWrite, false, false, false}, {"logic", LevelWrite, true, false, false}, {"aud", LevelWrite, true, true, false}, {"adm", LevelWrite, true, true, true}, } for _, c := range cases { if p.Level(c.user) != c.level { t.Errorf("%s: Level = %v, want %v", c.user, p.Level(c.user), c.level) } if p.CanEditLogic(c.user) != c.editLogic { t.Errorf("%s: CanEditLogic = %v, want %v", c.user, p.CanEditLogic(c.user), c.editLogic) } if p.CanViewAudit(c.user) != c.audit { t.Errorf("%s: CanViewAudit = %v, want %v", c.user, p.CanViewAudit(c.user), c.audit) } if p.CanAdmin(c.user) != c.admin { t.Errorf("%s: CanAdmin = %v, want %v", c.user, p.CanAdmin(c.user), c.admin) } } } func TestEffectiveRoleIsMaxAcrossGroups(t *testing.T) { p := New("", []GroupSpec{ spec("public", "", map[string]Role{"x": RoleViewer}), spec("a", "", map[string]Role{"x": RoleOperator}), spec("b", "", map[string]Role{"x": RoleAuditor}), }) if got := p.EffectiveRole("x"); got != RoleAuditor { t.Errorf("EffectiveRole(x) = %v, want auditor (max across groups)", got) } } func TestNestingInheritsMembership(t *testing.T) { // org → team-a → squad-1. A member of org is effectively in the descendants // too (membership flows parent→child), surfaced via GroupsOf for panel ACLs. p := New("", []GroupSpec{ spec("org", "", map[string]Role{"boss": RoleAdmin}), spec("team-a", "org", nil), spec("squad-1", "team-a", nil), }) groups := p.GroupsOf("boss") for _, want := range []string{"org", "team-a", "squad-1"} { if !containsStr(groups, want) { t.Errorf("GroupsOf(boss) = %v, missing %q", groups, want) } } } func TestMutationsRoundTrip(t *testing.T) { p := New("", []GroupSpec{ spec("public", "", map[string]Role{"root": RoleAdmin}), spec("ops", "", map[string]Role{"carol": RoleOperator}), }) // Set a user's full membership set: dave operator in ops, admin in eng (new). if err := p.SetUserRoles("dave", map[string]Role{"ops": RoleOperator, "eng": RoleAdmin}); err != nil { t.Fatal(err) } if g := p.GroupsOf("dave"); !containsStr(g, "ops") || !containsStr(g, "eng") { t.Errorf("GroupsOf(dave) = %v, want ops+eng", g) } if !p.CanAdmin("dave") { // admin in eng t.Error("CanAdmin(dave) = false after admin role in eng") } // Replacing membership removes dave from ops. if err := p.SetUserRoles("dave", map[string]Role{"eng": RoleAdmin}); err != nil { t.Fatal(err) } if containsStr(p.GroupsOf("dave"), "ops") { t.Error("dave still in ops after membership replaced") } // Rename carries dave's admin grant from eng → engineering. if err := p.RenameGroup("eng", "engineering"); err != nil { t.Fatal(err) } if !p.CanAdmin("dave") { t.Error("CanAdmin(dave) = false after eng renamed to engineering") } if !containsStr(p.GroupNames(), "engineering") || containsStr(p.GroupNames(), "eng") { t.Errorf("GroupNames after rename = %v", p.GroupNames()) } // Delete drops the group; dave loses admin but root (in public) keeps it. if err := p.DeleteGroup("engineering"); err != nil { t.Fatal(err) } if p.CanAdmin("dave") { t.Error("CanAdmin(dave) = true after engineering deleted") } if !p.CanAdmin("root") { t.Error("CanAdmin(root) = false; root grant in public should survive") } if err := p.DeleteGroup("nope"); err != ErrNotFound { t.Errorf("DeleteGroup(nope) = %v, want ErrNotFound", err) } } func TestPublicGroupProtected(t *testing.T) { p := New("", []GroupSpec{spec("public", "", map[string]Role{"a": RoleAdmin})}) if err := p.DeleteGroup(PublicGroup); err == nil { t.Error("DeleteGroup(public) = nil, want error") } if err := p.RenameGroup(PublicGroup, "other"); err == nil { t.Error("RenameGroup(public) = nil, want error") } if !containsStr(p.GroupNames(), PublicGroup) { t.Error("public group missing after protected mutations") } } func TestParentCycleRejected(t *testing.T) { p := New("", []GroupSpec{ spec("a", "", map[string]Role{"x": RoleViewer}), spec("b", "a", nil), }) // Making a's parent b would create a cycle a→b→a; it must be dropped to root. if err := p.SetGroup("a", "b", map[string]Role{"x": RoleViewer}); err != nil { t.Fatal(err) } snap := p.Snapshot() for _, g := range snap.Groups { if g.Name == "a" && g.Parent != "" { t.Errorf("group a parent = %q, want root (cycle should be dropped)", g.Parent) } } } func TestPersistenceRoundTrip(t *testing.T) { dir := t.TempDir() p := New("admin", []GroupSpec{ spec("public", "", map[string]Role{"alice": RoleLogic}), spec("ops", "", map[string]Role{"carol": RoleAdmin}), }) if err := p.EnablePersistence(dir); err != nil { t.Fatal(err) } // A mutation triggers the first write of access.json. if err := p.SetUserRoles("dave", map[string]Role{"ops": RoleOperator}); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "access.json")); err != nil { t.Fatalf("access.json not written: %v", err) } // A fresh policy loading the same dir should see the persisted state, not the // (different) seed it was constructed with. p2 := New("seed", []GroupSpec{spec("public", "", map[string]Role{"seeduser": RoleViewer})}) if err := p2.EnablePersistence(dir); err != nil { t.Fatal(err) } if p2.ResolveUser("") != "admin" { t.Errorf("default user = %q, want admin", p2.ResolveUser("")) } if !p2.CanEditLogic("alice") || p2.CanEditLogic("zed") { t.Error("logic-editor role not persisted") } if !p2.CanAdmin("carol") { t.Error("admin role not persisted") } if !containsStr(p2.GroupsOf("dave"), "ops") { t.Error("dave membership not persisted") } if p2.EffectiveRole("seeduser") != RoleViewer || p2.Level("seeduser") != LevelRead { // seeduser came from the discarded seed; should be an unlisted viewer now. t.Error("persisted state did not supersede the seed") } } func TestConcurrentAccess(t *testing.T) { p := New("", []GroupSpec{ spec("public", "", map[string]Role{"root": RoleAdmin}), spec("ops", "", map[string]Role{"carol": RoleOperator}), }) if err := p.EnablePersistence(t.TempDir()); err != nil { t.Fatal(err) } var wg sync.WaitGroup for i := 0; i < 8; i++ { wg.Add(2) go func() { defer wg.Done() for j := 0; j < 100; j++ { p.CanAdmin("carol") p.Level("carol") p.GroupsOf("carol") p.Snapshot() } }() go func() { defer wg.Done() for j := 0; j < 100; j++ { p.SetMemberRole("ops", "carol", RoleAuditor) p.SetUserRoles("carol", map[string]Role{"ops": RoleOperator, "eng": RoleAdmin}) p.RemoveMember("ops", "carol") } }() } wg.Wait() } func containsStr(xs []string, v string) bool { for _, x := range xs { if x == v { return true } } return false }