2026-06-22 16:06:57 +02:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"nadir/internal/module"
|
|
|
|
|
"nadir/internal/rbac"
|
|
|
|
|
|
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// fakeModule implements module.Module with a fixed permission set, so config
|
|
|
|
|
// tests don't depend on the concrete modules' exec behavior.
|
|
|
|
|
type fakeModule struct {
|
|
|
|
|
id string
|
|
|
|
|
perms []rbac.Permission
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f fakeModule) ID() string { return f.id }
|
|
|
|
|
func (f fakeModule) Permissions() []rbac.Permission { return f.perms }
|
|
|
|
|
func (f fakeModule) Register(huma.API) {}
|
|
|
|
|
|
|
|
|
|
func mods() []module.Module {
|
|
|
|
|
return []module.Module{
|
|
|
|
|
fakeModule{id: "system", perms: []rbac.Permission{rbac.Read, rbac.Write, rbac.Root}},
|
|
|
|
|
fakeModule{id: "services", perms: []rbac.Permission{rbac.Read, rbac.Write}},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:29:45 +02:00
|
|
|
func TestSecureCookieDefaultsFalse(t *testing.T) {
|
|
|
|
|
if (&File{}).SecureCookie() {
|
|
|
|
|
t.Error("omitted secure_tls should default to false")
|
2026-06-22 16:06:57 +02:00
|
|
|
}
|
2026-06-24 17:29:45 +02:00
|
|
|
yes := true
|
|
|
|
|
if !(&File{Server: Server{SecureTLS: &yes}}).SecureCookie() {
|
|
|
|
|
t.Error("secure_tls: true should enable the Secure flag")
|
2026-06-22 16:06:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoad(t *testing.T) {
|
|
|
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
|
|
|
os.WriteFile(path, []byte(`
|
|
|
|
|
server:
|
|
|
|
|
secure_tls: false
|
|
|
|
|
roles:
|
|
|
|
|
admin:
|
|
|
|
|
"*": ["*"]
|
|
|
|
|
assignments:
|
|
|
|
|
urania: [admin]
|
|
|
|
|
`), 0600)
|
|
|
|
|
|
|
|
|
|
f, err := Load(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if f.SecureCookie() {
|
|
|
|
|
t.Error("secure_tls: false not parsed")
|
|
|
|
|
}
|
|
|
|
|
if len(f.Roles["admin"]) == 0 || len(f.Assignments["urania"]) == 0 {
|
|
|
|
|
t.Error("roles/assignments not parsed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoadMissingFile(t *testing.T) {
|
|
|
|
|
if _, err := Load(filepath.Join(t.TempDir(), "nope.yaml")); err == nil {
|
|
|
|
|
t.Fatal("expected error for missing file")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestApplyValid(t *testing.T) {
|
|
|
|
|
f := &File{
|
|
|
|
|
Roles: map[string]map[string][]string{
|
|
|
|
|
"admin": {"*": {"*"}},
|
|
|
|
|
"auditor": {"*": {"read"}},
|
|
|
|
|
"sysop": {"system": {"read", "root"}},
|
|
|
|
|
},
|
|
|
|
|
Assignments: map[string][]string{"urania": {"admin"}, "bob": {"sysop"}},
|
|
|
|
|
}
|
|
|
|
|
roles := rbac.New()
|
|
|
|
|
if err := Apply(f, roles, mods()); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !roles.Can("urania", "services", rbac.Write) {
|
|
|
|
|
t.Error("admin wildcard not applied")
|
|
|
|
|
}
|
|
|
|
|
if !roles.Can("bob", "system", rbac.Root) || roles.Can("bob", "system", rbac.Write) {
|
|
|
|
|
t.Error("sysop grants not applied correctly")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestApplyErrors(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
f *File
|
|
|
|
|
}{
|
|
|
|
|
{"unknown module", &File{Roles: map[string]map[string][]string{
|
|
|
|
|
"r": {"firewall": {"read"}}}}},
|
|
|
|
|
{"unknown perm on known module", &File{Roles: map[string]map[string][]string{
|
|
|
|
|
"r": {"system": {"banana"}}}}},
|
|
|
|
|
{"wildcard module with perm no module exports", &File{Roles: map[string]map[string][]string{
|
|
|
|
|
"r": {"*": {"banana"}}}}},
|
|
|
|
|
{"assignment to unknown role", &File{
|
|
|
|
|
Roles: map[string]map[string][]string{"r": {"system": {"read"}}},
|
|
|
|
|
Assignments: map[string][]string{"u": {"ghost"}}}},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if err := Apply(tt.f, rbac.New(), mods()); err == nil {
|
|
|
|
|
t.Errorf("expected error for %s", tt.name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestApplyWildcardPermAlwaysOK(t *testing.T) {
|
|
|
|
|
f := &File{Roles: map[string]map[string][]string{"r": {"system": {"*"}}}}
|
|
|
|
|
if err := Apply(f, rbac.New(), mods()); err != nil {
|
|
|
|
|
t.Fatalf("wildcard permission should validate: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDefaultPathAndExpandPath(t *testing.T) {
|
|
|
|
|
defaultPath, err := DefaultPath()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("DefaultPath failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
expectedSuffix := filepath.Join("nadir", "config.yaml")
|
|
|
|
|
if !filepath.IsAbs(defaultPath) || !strings.HasSuffix(defaultPath, expectedSuffix) {
|
|
|
|
|
t.Errorf("expected default path to end with %q and be absolute, got %q", expectedSuffix, defaultPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expanded, err := ExpandPath("~/foo/config.yaml")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ExpandPath failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !filepath.IsAbs(expanded) || !strings.HasSuffix(expanded, filepath.Join("foo", "config.yaml")) {
|
|
|
|
|
t.Errorf("ExpandPath did not resolve ~/ correctly, got %q", expanded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plain := "/etc/nadir/config.yaml"
|
|
|
|
|
expandedPlain, err := ExpandPath(plain)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ExpandPath failed for plain path: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if expandedPlain != plain {
|
|
|
|
|
t.Errorf("expected no-op for %q, got %q", plain, expandedPlain)
|
|
|
|
|
}
|
|
|
|
|
}
|