Completes the migration. The embedded per-depth tree (internal/zddc/defaults/)
is now the sole source of the shipped baseline; defaults.zddc.yaml is deleted.
- EmbeddedDefaults() assembles the tree (no yaml). show-defaults now emits a
.zddc.zip (per-depth, "*" wildcard members) via EmbeddedDefaultsZip() —
operators redirect it to <ROOT>/.zddc.zip (or any directory) and edit/add/
delete individual members.
- Dropped EmbeddedDefaultsBytes; reworked the dumpable test to validate the
emitted zip; removed the now-redundant tree-vs-yaml oracle (the Layer-2
matrix is the ongoing behavioral guarantee, and it stays green).
- Swept stale "defaults.zddc.yaml" comment references to the embedded tree.
- GRAMMAR.md §1/§6 updated: .zddc.zip is a policy bundle mountable at ANY
directory (subtree mount; inherit:false + acl.inherit:false = island); the
shipped baseline is the embedded bundle at the root.
Net of the 6-phase migration: policy is per-depth .zddc files in a .zddc.zip
that an operator can drop at any level to override the cascade; the engine
(Assemble + the unchanged walker) enforces it. Full Go suite + matrix green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
310 lines
12 KiB
Go
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
|
|
// internal/zddc/defaults/ — 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 internal/zddc/defaults/) 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>/")
|
|
}
|
|
}
|