ZDDC/zddc/internal/zddc/standardroles_test.go
ZDDC 7d462ab7a4 test(zddc): update internal/zddc suite for flat-peer layout
Repoint default-tool/history/canonical-folder/auto-own/virtual/declared,
role-grant, and WORM-zone expectations to the top-level peer layout: archive
is now blanket-WORM (DC = rc there), the workspace/register peers carry the
DC grants directly, and incoming/working/staging/reviewing/mdl/rsk/ssr are
physical peers. ensure_test repointed to top-level paths + the virtual-reject
test inverted (peers are physical now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:47:04 -05:00

310 lines
12 KiB
Go

package zddc
import (
"os"
"path/filepath"
"testing"
)
// TestStandardRoles_DocControllerScopedCreate — DC authority comes
// PURELY from the cascade now (no subtree-admin / admins: list). The
// model:
// - rw at the project level (read + overwrite-existing, no `c`)
// - rwc at archive/ (can create party subfolders)
// - When DC mkdirs archive/<party>/, ensure.go writes an auto-own
// .zddc granting both the creator email AND the document_controller
// role rwcda there (via auto_own_roles in defaults). This test
// simulates that .zddc directly so the cascade behaviour can be
// asserted in isolation.
// - From the party's auto-own .zddc, the role rwcda cascades down to
// descendants by default; explicit slot grants (rwcd at incoming/
// and staging/) shadow it where the workflow needs `d`.
// - At received/issued (WORM): the WORM mask strips w/d/a from the
// inherited rwcda; the worm: list restores c → effective cr.
// - NOT subtree-admin anywhere — no admins: entries for the role.
func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
resetCache()
root := t.TempDir()
// DC authority comes PURELY from the cascade peer grants in
// defaults.zddc.yaml — no auto-own / admins: list. DCs are typically
// in project_team too (the *@example.com wildcard); the defaults
// restate document_controller at each peer so the within-level union
// gives the DC the higher grant.
writeZddc(t, root, `roles:
document_controller:
members: ["dc@example.com"]
project_team:
members: ["*@example.com"]
`)
resetCache()
dc := "dc@example.com"
j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }
mustVerbs := func(dir string, want string) {
t.Helper()
chain, err := EffectivePolicy(root, dir)
if err != nil {
t.Fatalf("EffectivePolicy(%q): %v", dir, err)
}
var got VerbSet
if g, inWorm := WormZoneGrant(chain, dc); inWorm {
got = (EffectiveVerbs(chain, dc) & VerbR) | (g & VerbsRC)
} else {
got = EffectiveVerbs(chain, dc)
}
if got.String() != want {
t.Errorf("doc controller verbs at %s = %q, want %q", dir[len(root):], got.String(), want)
}
}
// Project level: rw (no c).
mustVerbs(j(), "rw")
mustVerbs(j("random-folder"), "rw")
// archive subtree is WORM: read + worm-create only (w/d/a stripped).
mustVerbs(j("archive"), "rc")
mustVerbs(j("archive", "Acme"), "rc")
mustVerbs(j("archive", "Acme", "received"), "rc")
mustVerbs(j("archive", "Acme", "issued"), "rc")
// Workspace peers: full authority via the peer-level DC grant.
mustVerbs(j("working", "Acme"), "rwcda")
mustVerbs(j("staging", "Acme"), "rwcda")
mustVerbs(j("reviewing", "Acme"), "rwcda")
mustVerbs(j("incoming", "Acme"), "rwcd")
// Registers.
mustVerbs(j("mdl", "Acme"), "rwcd")
mustVerbs(j("rsk", "Acme"), "rwcd")
mustVerbs(j("ssr"), "rwc")
// NOT subtree-admin anywhere — no admins: grant for the role.
for _, p := range []string{j(), j("archive"), j("working", "Acme"), j("ssr"), j("mdl", "Acme")} {
if IsSubtreeAdmin(root, p, Principal{Email: dc, Elevated: true}) {
t.Errorf("doc controller should NOT be subtree-admin of %s (no admins: list anywhere)", p[len(root):])
}
}
}
// TestStandardRoles_DocControllerMultiDC — a second DC added to the
// role gets the SAME rwcda at every party that any DC created,
// because the auto-own .zddc grants the role (not just the creator's
// email) via auto_own_roles in defaults.
func TestStandardRoles_DocControllerMultiDC(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `roles:
document_controller:
members: ["dc1@example.com", "dc2@example.com"]
`)
// dc1 created the party folder; the auto-own .zddc lists both
// dc1 (creator email) and the document_controller role (from
// auto_own_roles in defaults).
partyDir := filepath.Join(root, "Proj", "archive", "Acme")
if err := os.MkdirAll(partyDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, partyDir, `acl:
permissions:
"dc1@example.com": rwcda
document_controller: rwcda
created_by: dc1@example.com
`)
resetCache()
chain, _ := EffectivePolicy(root, partyDir)
// dc1 (creator) has rwcda directly.
if got := EffectiveVerbs(chain, "dc1@example.com"); got.String() != "rwcda" {
t.Errorf("dc1 (creator) at party = %q, want rwcda", got.String())
}
// dc2 (non-creator) ALSO has rwcda via the role grant. This is
// the whole point of auto_own_roles — peer DCs share authority
// without admin status.
if got := EffectiveVerbs(chain, "dc2@example.com"); got.String() != "rwcda" {
t.Errorf("dc2 (peer) at party = %q, want rwcda (role grant from auto_own_roles)", got.String())
}
}
// TestStandardRoles_ProjectTeamReadOnlyExceptOwned — project_team gets
// r across the project, but the per-user working home's auto-own .zddc
// (rwcda for the creator) wins via deepest-match, so a team member has
// full rights in their own home and read-only elsewhere.
func TestStandardRoles_ProjectTeamReadOnlyExceptOwned(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `roles:
project_team:
members: ["*@example.com"]
`)
// Simulate the auto-own .zddc the file API would write at
// archive/Acme/working/alice@example.com/ (fenced via
// acl.inherit:false, creator-owned).
homeDir := filepath.Join(root, "Proj", "archive", "Acme", "working", "alice@example.com")
if err := os.MkdirAll(homeDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, homeDir, `acl:
inherit: false
permissions:
"alice@example.com": rwcda
created_by: alice@example.com
`)
resetCache()
alice := "alice@example.com"
bob := "bob@example.com"
// Alice (team member) inside her own home → rwcda.
chain, _ := EffectivePolicy(root, homeDir)
if got := EffectiveVerbs(chain, alice); got.String() != "rwcda" {
t.Errorf("alice in own home = %q, want rwcda", got.String())
}
// Bob (team member) inside Alice's fenced home → nothing (fence
// blocks the project-level project_team:r; bob isn't named in the
// fenced .zddc).
if got := EffectiveVerbs(chain, bob); got != 0 {
t.Errorf("bob in alice's fenced home = %q, want empty (fence blocks inherited grants)", got.String())
}
// Alice elsewhere in the project (not her home, not WORM) → r.
chain2, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme"))
if got := EffectiveVerbs(chain2, alice); got.String() != "r" {
t.Errorf("alice in archive/Acme = %q, want r", got.String())
}
// Alice CANNOT write to incoming/ — that's the counterparty's drop
// zone, QC'd by the document controller. project_team gets read only.
chain3, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme", "incoming"))
if got := EffectiveVerbs(chain3, alice); got.String() != "r" {
t.Errorf("alice in incoming/ = %q, want r (no create/write for project_team)", got.String())
}
}
// TestStandardRoles_ProjectTeamInFlightRatchet — the one-way handoff
// from working/ → staging/ → issued/ as the team member sees it:
// full work in working (cr at the slot + rwcda inside the fenced
// home), drop-only in staging (cr — no modify after the drop), drop
// inside auto-own iteration folder in reviewing (cr at the slot,
// rwcda inside the auto-owned sub-folder), read-only in received/
// issued (WORM zones) and incoming/ (counterparty drop zone).
func TestStandardRoles_ProjectTeamInFlightRatchet(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `roles:
project_team:
members: ["*@example.com"]
`)
alice := "alice@example.com"
mustVerbs := func(dir string, want string) {
t.Helper()
chain, err := EffectivePolicy(root, dir)
if err != nil {
t.Fatalf("EffectivePolicy(%q): %v", dir, err)
}
var got VerbSet
if g, inWorm := WormZoneGrant(chain, alice); inWorm {
got = (EffectiveVerbs(chain, alice) & VerbR) | (g & VerbsRC)
} else {
got = EffectiveVerbs(chain, alice)
}
if got.String() != want {
t.Errorf("project_team alice at %s = %q, want %q", dir[len(root):], got.String(), want)
}
}
j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }
mustVerbs(j("working", "Acme"), "rc") // create + read at the workspace
mustVerbs(j("staging", "Acme"), "rc") // drop + read, no modify
mustVerbs(j("reviewing", "Acme"), "rc") // create iteration folders
mustVerbs(j("archive", "Acme", "received"), "r") // WORM — read pass-through
mustVerbs(j("archive", "Acme", "issued"), "r") // WORM — same
mustVerbs(j("incoming", "Acme"), "r") // counterparty drop zone — read only
}
// TestStandardRoles_DocControllerStagingDelete — DC needs `d` at
// staging/ to perform the staging-to-issued transfer (cut, not copy);
// the explicit document_controller: rwcd grant supplies it. Mirrors
// the incoming/ pattern (line 286-288 of defaults.zddc.yaml) where
// the QC + transfer-out workflow needed the same.
func TestStandardRoles_DocControllerStagingDelete(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `roles:
document_controller:
members: ["dc@example.com"]
`)
dc := "dc@example.com"
chain, err := EffectivePolicy(root, filepath.Join(root, "Proj", "staging", "Acme"))
if err != nil {
t.Fatalf("EffectivePolicy: %v", err)
}
got := EffectiveVerbs(chain, dc)
for _, v := range []VerbSet{VerbR, VerbW, VerbC, VerbD} {
if !got.Has(v) {
t.Errorf("doc controller at staging/ missing verb %q in %q (need rwcd for transfer-to-issued)", v.String(), got.String())
}
}
}
// TestStandardRoles_ObserverReadOnlyEverywhere — observer is the
// project-wide read-only role for auditors / regulators / external
// viewers. Unlike project_team, an observer must not contribute
// content anywhere: no create at archive/, no create at working/,
// no worm-create at received/issued, and not subtree-admin of
// anything. Read passes through WORM zones (worm: lists strip w/d/a
// but never r).
func TestStandardRoles_ObserverReadOnlyEverywhere(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `roles:
observer:
members: ["auditor@example.com"]
`)
obs := "auditor@example.com"
mustVerbs := func(dir string, want string) {
t.Helper()
chain, err := EffectivePolicy(root, dir)
if err != nil {
t.Fatalf("EffectivePolicy(%q): %v", dir, err)
}
// Mirror InternalDecider.Allow's WORM-aware composition so the
// assertion covers received/issued correctly.
var got VerbSet
if g, inWorm := WormZoneGrant(chain, obs); inWorm {
got = (EffectiveVerbs(chain, obs) & VerbR) | (g & VerbsRC)
} else {
got = EffectiveVerbs(chain, obs)
}
if got.String() != want {
t.Errorf("observer verbs at %s = %q, want %q", dir[len(root):], got.String(), want)
}
}
// Project level: read-only.
mustVerbs(filepath.Join(root, "Proj"), "r")
// A random subfolder under the project still read-only.
mustVerbs(filepath.Join(root, "Proj", "random-folder"), "r")
// archive/ — read-only (no create at the party-folder level).
mustVerbs(filepath.Join(root, "Proj", "archive"), "r")
// incoming/ — read-only (no create even though incoming/ has
// drop_target and auto_own; the cascade ACL still gates create).
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "incoming"), "r")
// In-flight lifecycle slots — read-only.
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "working"), "r")
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "staging"), "r")
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "reviewing"), "r")
// WORM zones — read passes through; no worm-create (observer is
// not in the worm: list).
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "r")
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "r")
// Observer is not subtree-admin of anything in the project — even
// when notionally elevated, the role carries no admin grant.
if IsSubtreeAdmin(root, filepath.Join(root, "Proj"), Principal{Email: obs, Elevated: true}) {
t.Errorf("observer should NOT be subtree-admin of the project root")
}
if IsSubtreeAdmin(root, filepath.Join(root, "Proj", "archive"), Principal{Email: obs, Elevated: true}) {
t.Errorf("observer should NOT be subtree-admin of archive/")
}
if IsSubtreeAdmin(root, filepath.Join(root, "Proj", "archive", "Acme"), Principal{Email: obs, Elevated: true}) {
t.Errorf("observer should NOT be subtree-admin of archive/<party>/")
}
}