Implemented admin pane + user permission
This commit is contained in:
+257
-26
@@ -1,35 +1,266 @@
|
||||
package access
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCanEditLogic(t *testing.T) {
|
||||
groups := map[string][]string{"ops": {"carol"}}
|
||||
// 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}
|
||||
}
|
||||
|
||||
// No allowlist configured: everyone with write access may edit logic.
|
||||
open := New("", nil, groups, nil, nil)
|
||||
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 !open.CanEditLogic(u) {
|
||||
t.Errorf("unrestricted: CanEditLogic(%q) = false, want true", u)
|
||||
if !p.CanAdmin(u) {
|
||||
t.Errorf("unconfigured: CanAdmin(%q) = false, want true", u)
|
||||
}
|
||||
}
|
||||
if open.LogicRestricted() {
|
||||
t.Error("LogicRestricted() = true with no allowlist")
|
||||
}
|
||||
|
||||
// Allowlist by username and by group name.
|
||||
p := New("", nil, groups, []string{"alice", "ops"}, nil)
|
||||
if !p.LogicRestricted() {
|
||||
t.Error("LogicRestricted() = false with allowlist set")
|
||||
}
|
||||
cases := map[string]bool{
|
||||
"": true, // anonymous / trusted LAN
|
||||
"alice": true, // listed user
|
||||
"carol": true, // member of listed group "ops"
|
||||
"bob": false, // not listed
|
||||
}
|
||||
for u, want := range cases {
|
||||
if got := p.CanEditLogic(u); got != want {
|
||||
t.Errorf("CanEditLogic(%q) = %v, want %v", u, got, want)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user