Implemented admin pane + user permission

This commit is contained in:
Martino Ferrari
2026-06-22 17:49:14 +02:00
parent 73fcbe7b28
commit ac24011487
67 changed files with 5925 additions and 411 deletions
+257 -26
View File
@@ -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
}