refactor(zddc): worm: is a list of principals, not a {principal: verbs} map

Per design feedback: the verb string in a worm: entry was always
effectively "cr" (the key's whole job is to restore write-once-create
inside the locked zone, and you need read to see what you filed), so
spelling it out per-entry was redundant. worm: is now just a list of
principal patterns — email-globs, @role:name, or bare role names —
and every listed principal gets read + write-once-create. An empty
list ([]) still marks the WORM zone with no create-capable
principals.

Changes:
  - ZddcFile.Worm: map[string]string → []string
  - mergeOverlay: concat-dedupe (a deeper .zddc adds controllers);
    mergeStringSlicePreserveEmpty keeps `worm: []` non-nil through
    the overlay so it still marks the zone
  - WormZoneGrant: walks the list, grants VerbsRC to each matching
    principal; result is always ⊆ {r, c}
  - ValidateFile: validates each entry as an email-glob (role refs
    skipped — validated by the role machinery)
  - defaults.zddc.yaml: received/ and issued/ carry `worm: []`
  - tests updated to the list form (worm_test.go, fileapi_test.go)

All Go tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-12 09:40:15 -05:00
parent 918f330a6f
commit 2de2fdf92c
8 changed files with 102 additions and 121 deletions

View file

@ -511,13 +511,10 @@ func TestFileAPI_WORM_DocControllerNeedsExplicitGrant(t *testing.T) {
t.Fatalf("dc without explicit grant → issued: want 403, got %d: %s", rec.Code, rec.Body.String())
}
// Operator places an explicit worm: grant at archive/Acme/issued/.zddc
// naming the document-controller role. That principal then gets
// {r, c} inside the WORM zone — the embedded `worm: {}` (no
// controllers) is unioned with this deeper grant.
issuedZ := []byte(`worm:
_doc_controller: cr
`)
// Operator names the document-controller role in the issued/ WORM
// zone. That role's members then get {r, c} there — the embedded
// `worm: []` (no controllers) is unioned with this deeper grant.
issuedZ := []byte("worm:\n - _doc_controller\n")
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
t.Fatalf("write issued .zddc: %v", err)
}
@ -605,8 +602,9 @@ func TestFileAPI_AutoMkdirOwnership(t *testing.T) {
func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) {
_, do, root := rolePermissionsTestSetup(t)
// Place an explicit worm: grant so dc has cr in the issued WORM zone.
issuedZ := []byte("worm:\n _doc_controller: cr\n")
// Name the document-controller role in the issued/ WORM zone so its
// members get cr there.
issuedZ := []byte("worm:\n - _doc_controller\n")
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
t.Fatalf("seed issued .zddc: %v", err)
}

View file

@ -1300,7 +1300,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-12 13:28:43 · 9c7858c-dirty</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-12 14:40:09 · 918f330-dirty</span></span>
</div>
</div>
<div class="header-right">

View file

@ -71,34 +71,33 @@ paths:
# Browse shows a drag-drop overlay here.
drop_target: true
# received/ and issued/ are WORM (write-once-read-many).
# The `worm:` key marks the zone:
# The `worm:` list marks the zone:
#
# - write (w) and delete (d) are stripped for EVERYONE
# - create (c) is stripped for everyone EXCEPT the
# principals listed below (none, in the baseline)
# - read (r) is whatever the normal cascade ACL granted
# — the WORM key does NOT itself grant read, so a
# deployment that restricts read keeps that
# restriction; one that grants read to the project
# keeps that too
# - admins bypass entirely (handler IsAdmin escape)
# principals listed — they get read + write-once-
# create ("cr")
# - read for non-listed principals is whatever the
# normal cascade ACL granted; the WORM list does not
# itself confer read to outsiders
# - admins (root / subtree) bypass entirely — the
# human escape hatch for mis-filed documents
#
# The empty map ({}) means "WORM, no create-capable
# principals". A deployment names its document controller
# by placing a .zddc at received/ (or issued/, or
# archive/<party>/, or wherever scopes it right) with:
# The baseline is an empty list: WORM zone, no
# create-capable principals — filing is locked until a
# deployment names a document controller, e.g.
#
# worm:
# "doc-control@example.com": cr # read + write-once
# worm: ["doc-control@example.com"]
#
# worm: grants UNION across the cascade, so multiple
# controllers (or a deeper-scoped one) compose.
# at received/ (or issued/, or archive/<party>/, or
# wherever scopes it right). worm: lists UNION across the
# cascade, so a deeper .zddc adds more controllers.
received:
default_tool: archive
worm: {}
worm: []
issued:
default_tool: archive
worm: {}
worm: []
working:
default_tool: mdedit
available_tools: [mdedit, classifier]

View file

@ -210,32 +210,31 @@ type ZddcFile struct {
DropTarget *bool `yaml:"drop_target,omitempty" json:"drop_target,omitempty"`
// Worm marks this directory (and its descendants) as
// write-once-read-many. A non-nil Worm map — even an empty one —
// write-once-read-many. A non-nil Worm list — even an empty one —
// puts the path into a WORM zone with these effects, applied AFTER
// the normal cascade ACL and BEFORE any admin escape hatch:
//
// - write (w) and delete (d) are stripped for everyone
// - create (c) is stripped for everyone EXCEPT principals named
// in the Worm map with a verb string containing 'c'
// - read (r) survives if the normal cascade ACL granted it, OR
// if the Worm map grants it ('r' in the principal's verb
// string) — so a document controller who isn't in the project
// ACL can still be granted read+create here
// - create (c) is stripped for everyone EXCEPT the principals
// listed here — they get read + write-once-create ("cr")
// - read (r) for non-listed principals is whatever the normal
// cascade ACL granted (the WORM list does not itself confer
// read to outsiders, only to its own members)
//
// Map shape mirrors acl.permissions: keys are email-glob patterns
// (or @role:<name>), values are verb strings restricted to the
// subset {r, c}. An empty map ({}) is a WORM zone with no
// create-capable principals — useful as the embedded baseline,
// which a deployment overrides by placing a .zddc with
// `worm: {"doc-control@example.com": cr}` at the WORM folder (or
// deeper). Worm grants UNION across the cascade — deeper .zddc
// can add more controllers.
// Each entry is an email-glob pattern (or @role:<name> / a bare
// role name). An empty list ([]) is a WORM zone with no
// create-capable principals — the embedded baseline ships this
// on received/ and issued/ with the `document_controller` role
// named but member-empty, so a deployment enables filing simply
// by populating that role. Worm lists UNION across the cascade —
// a deeper .zddc adds more controllers.
//
// Admins (root or subtree) bypass the WORM mask entirely; the
// handler does the IsAdmin / IsSubtreeAdmin check before invoking
// the policy evaluator. WORM is a normal-user constraint, not an
// absolute one — mis-filed documents still need a human escape.
Worm map[string]string `yaml:"worm,omitempty" json:"worm,omitempty"`
// Admins (root or subtree) bypass the WORM constraint entirely;
// the handler does the IsAdmin / IsSubtreeAdmin check before
// invoking the policy evaluator. WORM is a normal-user
// constraint, not an absolute one — mis-filed documents still
// need a human escape.
Worm []string `yaml:"worm,omitempty" json:"worm,omitempty"`
// AvailableTools restricts which tools the server will auto-serve
// at this directory and its descendants. The effective list is the

View file

@ -248,32 +248,22 @@ func ValidateFile(zf ZddcFile) []FieldError {
})
}
}
// worm: keys are email-glob patterns (or @role:name); values are
// verb strings restricted to {r, c} — write/delete/admin are
// meaningless inside a WORM zone and rejecting them at write time
// avoids a silently-ineffective entry.
for principal, verbStr := range zf.Worm {
fld := fmt.Sprintf("worm.%s", principal)
// @role: prefixes are validated by the role machinery; only
// check the bare-email-glob form here.
if !strings.HasPrefix(principal, "@role:") {
// worm: is a list of principal patterns (email-globs, @role:name,
// or bare role names) that get write-once-create inside the WORM
// zone. Validate each as an email-glob unless it's a role
// reference (role names are validated by the role machinery).
for i, principal := range zf.Worm {
if strings.HasPrefix(principal, "@role:") {
continue // role refs validated elsewhere
}
// A bare name with no "@" could be a role name; ValidatePattern
// accepts it (no @, no whitespace), and MatchesPrincipal
// resolves it as a role if one is defined. So this only
// rejects genuinely malformed entries (whitespace, double @).
if err := ValidatePattern(principal); err != nil {
errs = append(errs, FieldError{Field: fld, Message: err.Error()})
continue
}
}
v, ok := ParseVerbSet(verbStr)
if !ok {
errs = append(errs, FieldError{
Field: fld,
Message: fmt.Sprintf("invalid verb string %q (allowed: r, c)", verbStr),
})
continue
}
if v&^VerbsRC != 0 {
errs = append(errs, FieldError{
Field: fld,
Message: fmt.Sprintf("verb string %q includes w/d/a — only r and c are valid inside a WORM zone", verbStr),
Field: fmt.Sprintf("worm[%d]", i),
Message: err.Error(),
})
}
}

View file

@ -79,10 +79,11 @@ func mergeOverlay(base, top ZddcFile) ZddcFile {
out.DropTarget = top.DropTarget
}
// Worm: presence (non-nil, even empty) marks the WORM zone.
// Merge per-key (top wins on clash); preserve a non-nil empty
// map so `worm: {}` survives the overlay.
// Concat-dedupe across levels (a deeper .zddc adds controllers);
// preserve a non-nil empty slice so `worm: []` survives the
// overlay.
if top.Worm != nil {
out.Worm = mergeStringMapPreserveEmpty(out.Worm, top.Worm)
out.Worm = mergeStringSlicePreserveEmpty(out.Worm, top.Worm)
}
if top.Virtual != nil {
out.Virtual = top.Virtual
@ -141,17 +142,26 @@ func mergeStringMap(base, top map[string]string) map[string]string {
return out
}
// mergeStringMapPreserveEmpty is mergeStringMap but always returns a
// non-nil result when top is non-nil — so an empty `worm: {}` in a
// mergeStringSlicePreserveEmpty is mergeStringSlice but always returns
// a non-nil result when top is non-nil — so an empty `worm: []` in a
// .zddc still marks the WORM zone after the overlay. Caller is
// expected to only invoke this when top != nil.
func mergeStringMapPreserveEmpty(base, top map[string]string) map[string]string {
out := make(map[string]string, len(base)+len(top))
for k, v := range base {
out[k] = v
func mergeStringSlicePreserveEmpty(base, top []string) []string {
seen := make(map[string]struct{}, len(base)+len(top))
out := make([]string, 0, len(base)+len(top))
for _, v := range base {
if _, ok := seen[v]; ok {
continue
}
for k, v := range top {
out[k] = v
seen[v] = struct{}{}
out = append(out, v)
}
for _, v := range top {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}

View file

@ -31,23 +31,15 @@ package zddc
// grant. Admins are excluded upstream (handler's IsAdmin bypass).
func WormZoneGrant(chain PolicyChain, email string, mode CascadeMode) (grant VerbSet, inWorm bool) {
for i := 0; i < len(chain.Levels); i++ {
wm := chain.Levels[i].Worm
if wm == nil {
wl := chain.Levels[i].Worm
if wl == nil {
continue
}
inWorm = true
for principal, verbStr := range wm {
if !MatchesPrincipal(principal, email, chain, i, mode) {
continue
for _, principal := range wl {
if MatchesPrincipal(principal, email, chain, i, mode) {
grant |= VerbsRC // listed controllers get read + write-once-create
}
v, ok := ParseVerbSet(verbStr)
if !ok {
// Malformed verb string in operator YAML — ignore the
// entry rather than crash; ValidateFile flags it at
// write time so this shouldn't reach a healthy server.
continue
}
grant |= v
}
}
// The embedded baseline could in principle carry a top-level
@ -55,17 +47,14 @@ func WormZoneGrant(chain PolicyChain, email string, mode CascadeMode) (grant Ver
// fold it in for completeness.
if chain.Embedded.Worm != nil {
inWorm = true
for principal, verbStr := range chain.Embedded.Worm {
if !MatchesPattern(principal, email) {
continue
}
if v, ok := ParseVerbSet(verbStr); ok {
grant |= v
for _, principal := range chain.Embedded.Worm {
if MatchesPattern(principal, email) {
grant |= VerbsRC
}
}
}
if !inWorm {
return 0, false
}
return grant & VerbsRC, true
return grant, true
}

View file

@ -7,7 +7,7 @@ import (
)
// TestWormZoneGrant_EmbeddedConvention — archive/<party>/received and
// issued carry `worm: {}` in defaults.zddc.yaml, so any path under
// issued carry `worm: []` in defaults.zddc.yaml, so any path under
// those folders is a WORM zone (inWorm=true) with no create-capable
// principals (grant=0). Other paths are not WORM zones.
func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
@ -43,8 +43,8 @@ func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
// TestWormZoneGrant_OperatorGrantsController — a deployment grants a
// document controller create-once by placing a .zddc with a `worm:`
// entry at (or below) the WORM folder. That principal then gets
// {r, c} from WormZoneGrant; everyone else still gets 0.
// entry naming them at (or below) the WORM folder. That principal then
// gets {r, c} from WormZoneGrant; everyone else still gets 0.
func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
resetCache()
root := t.TempDir()
@ -52,13 +52,12 @@ func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
if err := os.MkdirAll(issuedDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, issuedDir, "worm:\n \"doc-control@example.com\": cr\n")
writeZddc(t, issuedDir, "worm:\n - doc-control@example.com\n")
chain, err := EffectivePolicy(root, issuedDir)
if err != nil {
t.Fatal(err)
}
// Controller gets {r, c}.
g, inWorm := WormZoneGrant(chain, "doc-control@example.com", ModeDelegated)
if !inWorm {
t.Fatalf("inWorm = false, want true")
@ -66,29 +65,27 @@ func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
if g != VerbsRC {
t.Errorf("controller grant = %v, want rc", g)
}
// Someone else gets nothing from the worm map.
g2, _ := WormZoneGrant(chain, "rando@example.com", ModeDelegated)
if g2 != 0 {
t.Errorf("non-controller grant = %v, want 0", g2)
}
}
// TestWormZoneGrant_MasksWriteDelete — even if a worm: entry tried to
// grant w/d/a (which ValidateFile rejects, but defense in depth),
// WormZoneGrant strips it to {r, c}.
func TestWormZoneGrant_MasksWriteDelete(t *testing.T) {
// TestWormZoneGrant_GrantIsAlwaysRC — a worm: entry never confers
// more than {r, c} no matter what (the list form can't even express
// w/d, but verifying the constant the resolver uses).
func TestWormZoneGrant_GrantIsAlwaysRC(t *testing.T) {
resetCache()
root := t.TempDir()
rec := filepath.Join(root, "Proj", "archive", "Acme", "received")
if err := os.MkdirAll(rec, 0o755); err != nil {
t.Fatal(err)
}
// Hand-written with rwcda — should be masked to rc by WormZoneGrant.
writeZddc(t, rec, "worm:\n \"x@example.com\": rwcda\n")
writeZddc(t, rec, "worm:\n - x@example.com\n")
chain, _ := EffectivePolicy(root, rec)
g, _ := WormZoneGrant(chain, "x@example.com", ModeDelegated)
if g != VerbsRC {
t.Errorf("grant = %v (%s), want rc — w/d/a must be masked", g, g.String())
t.Errorf("grant = %v (%s), want rc", g, g.String())
}
}
@ -104,9 +101,8 @@ func TestWormZoneGrant_GrantsUnionAcrossCascade(t *testing.T) {
if err := os.MkdirAll(rec, 0o755); err != nil {
t.Fatal(err)
}
// Party-level worm grant for alice; received-level worm grant for bob.
writeZddc(t, party, "worm:\n \"alice@example.com\": cr\n")
writeZddc(t, rec, "worm:\n \"bob@example.com\": c\n")
writeZddc(t, party, "worm:\n - alice@example.com\n")
writeZddc(t, rec, "worm:\n - bob@example.com\n")
chain, _ := EffectivePolicy(root, rec)
ga, inA := WormZoneGrant(chain, "alice@example.com", ModeDelegated)
@ -114,7 +110,7 @@ func TestWormZoneGrant_GrantsUnionAcrossCascade(t *testing.T) {
t.Errorf("alice grant = %v inWorm=%v, want rc/true", ga, inA)
}
gb, _ := WormZoneGrant(chain, "bob@example.com", ModeDelegated)
if gb&VerbC == 0 {
t.Errorf("bob grant = %v, want at least c", gb)
if gb != VerbsRC {
t.Errorf("bob grant = %v, want rc", gb)
}
}