ZDDC/zddc/internal/zddc/admin_test.go
ZDDC e44ccc3500 feat(zddc-server): delegated subtree admins + built-in .zddc editor
Generalize the admin model from "single root super-admin" to a
delegated chain: a `<dir>/.zddc/admins` list grants admin authority
for that subtree, with a strict-ancestor rule preventing
self-elevation (you cannot edit the .zddc that grants your own
authority — only files strictly below it).

Add a guided server-rendered editor at /.admin/zddc/edit?path=<dir>
so subtree admins can manage their fiefdoms without filesystem
access. JSON API at /.admin/zddc covers GET (file + effective chain
+ can_edit), POST (atomic write + cache invalidation), DELETE,
plus a /tree endpoint listing every .zddc visible to the caller.
Optional theming via <root>/.admin.css.

Validation: glob syntax check, root-self-demotion rejection,
reserved-prefix path guard, YAML round-trip sanity. Writes are
atomic (temp file + fsync + rename) and invalidate the policy
cache.

Also includes the prior in-flight `Title` field on ProjectInfo
so per-project .zddc titles surface on the landing-page picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:52:06 -05:00

404 lines
11 KiB
Go

package zddc
import (
"os"
"path/filepath"
"testing"
)
func TestIsAdmin(t *testing.T) {
cases := []struct {
name string
zddcBody string // contents of <root>/.zddc; empty string means no file
email string
want bool
}{
{
name: "no zddc file → not admin",
zddcBody: "",
email: "alice@example.com",
want: false,
},
{
name: "zddc file with no admins key → not admin",
zddcBody: "acl:\n allow: [\"*\"]\n",
email: "alice@example.com",
want: false,
},
{
name: "zddc file with empty admins list → not admin",
zddcBody: "admins: []\n",
email: "alice@example.com",
want: false,
},
{
name: "exact-match admin → admin",
zddcBody: "admins:\n - alice@example.com\n",
email: "alice@example.com",
want: true,
},
{
name: "domain glob admin → admin",
zddcBody: "admins:\n - \"*@example.com\"\n",
email: "alice@example.com",
want: true,
},
{
name: "domain glob admin, wrong domain → not admin",
zddcBody: "admins:\n - \"*@example.com\"\n",
email: "alice@other.org",
want: false,
},
{
name: "non-matching listed admin → not admin",
zddcBody: "admins:\n - bob@example.com\n",
email: "alice@example.com",
want: false,
},
{
name: "empty email never matches even if pattern is *",
zddcBody: "admins:\n - \"*\"\n",
email: "",
want: false,
},
{
name: "acl deny does not affect admins",
zddcBody: "acl:\n deny: [\"*@example.com\"]\nadmins:\n - alice@example.com\n",
email: "alice@example.com",
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
if tc.zddcBody != "" {
if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte(tc.zddcBody), 0o644); err != nil {
t.Fatalf("write .zddc: %v", err)
}
}
if got := IsAdmin(root, tc.email); got != tc.want {
t.Errorf("IsAdmin(%q, %q) = %v, want %v", root, tc.email, got, tc.want)
}
})
}
}
// TestIsAdminSubdirIgnored documents that admins entries in subdirectory
// .zddc files are NOT honored by IsAdmin — only the root .zddc grants the
// server-wide super-admin role. Subtree admin authority for "fiefdom"
// editing is a separate concept covered by IsSubtreeAdmin / CanEditZddc.
func TestIsAdminSubdirIgnored(t *testing.T) {
root := t.TempDir()
sub := filepath.Join(root, "project")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
// Root has no admins; subdir tries to grant admin.
if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("acl:\n allow: [\"*\"]\n"), 0o644); err != nil {
t.Fatalf("write root .zddc: %v", err)
}
if err := os.WriteFile(filepath.Join(sub, ".zddc"), []byte("admins:\n - mallory@example.com\n"), 0o644); err != nil {
t.Fatalf("write subdir .zddc: %v", err)
}
if IsAdmin(root, "mallory@example.com") {
t.Error("subdir .zddc admins entry was honored — that is a privilege-escalation hole")
}
}
// fixture writes a tree of .zddc files. Keys are paths relative to root;
// the empty string means root itself ("<root>/.zddc"). Values are file
// contents. Intermediate directories are created as needed. Each path is
// joined with ".zddc".
func writeZddcTree(t *testing.T, root string, files map[string]string) {
t.Helper()
for rel, body := range files {
dir := filepath.Join(root, rel)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
// Always invalidate cache before/after writes inside a test so
// subsequent calls re-read disk. Tests run with t.TempDir() so
// there's no cross-test contamination, but the in-process cache
// is global and may carry stale entries between subtests if a
// prior subtest read the same path.
InvalidateCache(dir)
if body == "" {
continue
}
if err := os.WriteFile(filepath.Join(dir, ".zddc"), []byte(body), 0o644); err != nil {
t.Fatalf("write %s/.zddc: %v", rel, err)
}
}
}
func TestIsSubtreeAdmin(t *testing.T) {
cases := []struct {
name string
files map[string]string
dir string // relative to root
email string
want bool
}{
{
name: "no zddc anywhere → not admin",
files: map[string]string{},
dir: "",
email: "alice@example.com",
want: false,
},
{
name: "root admin → admin of any subtree",
files: map[string]string{
"": "admins:\n - alice@example.com\n",
"projects/x": "",
},
dir: "projects/x",
email: "alice@example.com",
want: true,
},
{
name: "subtree admin granted at intermediate level",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
"projects/x": "",
},
dir: "projects/x",
email: "alice@example.com",
want: true,
},
{
name: "subtree admin granted at the leaf level itself",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
},
dir: "projects",
email: "alice@example.com",
want: true,
},
{
name: "non-admin in same subtree → not admin",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
},
dir: "projects",
email: "bob@example.com",
want: false,
},
{
name: "admin granted in sibling subtree does not leak",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"foo": "admins:\n - alice@example.com\n",
"bar": "",
},
dir: "bar",
email: "alice@example.com",
want: false,
},
{
name: "glob admin",
files: map[string]string{
"": "admins:\n - \"*@varasys.io\"\n",
"projects": "",
},
dir: "projects",
email: "alice@varasys.io",
want: true,
},
{
name: "empty email never admin",
files: map[string]string{"": "admins:\n - \"*\"\n"},
dir: "",
email: "",
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
writeZddcTree(t, root, tc.files)
dir := filepath.Join(root, tc.dir)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir target: %v", err)
}
InvalidateCache(dir)
if got := IsSubtreeAdmin(root, dir, tc.email); got != tc.want {
t.Errorf("IsSubtreeAdmin(%q, %q) = %v, want %v",
tc.dir, tc.email, got, tc.want)
}
})
}
}
func TestCanEditZddc(t *testing.T) {
cases := []struct {
name string
files map[string]string
dir string
email string
want bool
}{
{
name: "root super-admin can edit root .zddc (bootstrap)",
files: map[string]string{
"": "admins:\n - root@example.com\n",
},
dir: "",
email: "root@example.com",
want: true,
},
{
name: "non-admin cannot edit root",
files: map[string]string{
"": "admins:\n - root@example.com\n",
},
dir: "",
email: "alice@example.com",
want: false,
},
{
name: "no zddc files at all → nobody edits root",
files: map[string]string{},
dir: "",
email: "anyone@example.com",
want: false,
},
{
name: "root super-admin can edit any subtree file",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects/x": "",
},
dir: "projects/x",
email: "root@example.com",
want: true,
},
{
name: "subtree admin can edit deeper file (strict ancestor satisfied)",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
"projects/x": "",
},
dir: "projects/x",
email: "alice@example.com",
want: true,
},
{
name: "subtree admin CANNOT edit their own grant file (no strict ancestor for them)",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
},
dir: "projects",
email: "alice@example.com",
want: false,
},
{
name: "subtree admin CANNOT edit root",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
},
dir: "",
email: "alice@example.com",
want: false,
},
{
name: "subtree admin CANNOT edit sibling's grant file",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"foo": "admins:\n - alice@example.com\n",
"bar": "admins:\n - bob@example.com\n",
},
dir: "bar",
email: "alice@example.com",
want: false,
},
{
name: "two-level delegation — mid-level admin edits leaf below their grant",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
"projects/sub": "admins:\n - bob@example.com\n",
"projects/sub/x": "",
},
dir: "projects/sub/x",
email: "alice@example.com",
want: true,
},
{
name: "two-level delegation — bob (mid-level admin) cannot edit own grant",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
"projects/sub": "admins:\n - bob@example.com\n",
},
dir: "projects/sub",
email: "bob@example.com",
want: false,
},
{
name: "two-level delegation — bob can still edit deeper",
files: map[string]string{
"": "admins:\n - root@example.com\n",
"projects": "admins:\n - alice@example.com\n",
"projects/sub": "admins:\n - bob@example.com\n",
"projects/sub/x": "",
},
dir: "projects/sub/x",
email: "bob@example.com",
want: true,
},
{
name: "mallory in a subdir admins list — original escalation case stays blocked",
files: map[string]string{
"": "acl:\n allow: [\"*\"]\n",
"project": "admins:\n - mallory@example.com\n",
},
dir: "project",
email: "mallory@example.com",
want: false,
},
{
name: "glob root admin can edit anything",
files: map[string]string{
"": "admins:\n - \"*@varasys.io\"\n",
"projects/x": "",
},
dir: "projects/x",
email: "alice@varasys.io",
want: true,
},
{
name: "empty email never edits",
files: map[string]string{"": "admins:\n - \"*\"\n"},
dir: "",
email: "",
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
writeZddcTree(t, root, tc.files)
dir := filepath.Join(root, tc.dir)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir target: %v", err)
}
InvalidateCache(dir)
if got := CanEditZddc(root, dir, tc.email); got != tc.want {
t.Errorf("CanEditZddc(dir=%q, email=%q) = %v, want %v",
tc.dir, tc.email, got, tc.want)
}
})
}
}