feat(zddc): WORM as a cascade key (worm:), retiring hardcoded path predicates

WORM (write-once-read-many) is no longer a special folder type keyed
off the literal names "received"/"issued". It's a cascade key —
`worm:` on any directory's .zddc — with the ACL-shaped semantics the
user described.

Schema:
  worm:
    "doc-control@example.com": cr   # email-glob or @role:name → verbs ⊆ {r, c}
  # an empty map ({}) is a WORM zone with no create-capable principals

Effect inside a WORM zone (any cascade level declares worm:), applied
AFTER the normal cascade ACL and BEFORE the admin escape hatch:
  - w / d / a stripped for everyone
  - c survives only via the worm: map
  - r survives via the normal ACL OR the worm: map (so a document
    controller who isn't in the project ACL still gets read+create)
  - worm: grants UNION across the cascade — deeper .zddc can name
    more controllers
  - admins (root / subtree) bypass entirely — handler does the
    IsAdmin check before the policy evaluator

defaults.zddc.yaml: archive/<party>/received and archive/<party>/issued
carry `worm: {}` (WORM zone, no controllers — the deployment names
its document controller by adding a deeper .zddc with
`worm: {<principal>: cr}`). The canonical convention is unchanged;
the difference is an operator can now mark any directory WORM, or
rename received/issued, without a code change.

Removed (hardcoded path predicates, superseded by the cascade walk):
  zddc.IsWormPath
  zddc.WormFolderLevelIndex
  zddc.splitPathSegments  (only IsWormPath used it)
Kept: zddc.WormMask (generic verb-set primitive), zddc.VerbsRC.

New:
  zddc.WormZoneGrant(chain, email, mode) → (verbs, inWormZone)
    Walks the chain for worm: declarations; unions the principal's
    grants masked to {r, c}.
  policy.InternalDecider.Allow: WORM block rewritten to consult
    WormZoneGrant instead of IsWormPath/WormFolderLevelIndex.
  ValidateFile: worm: keys validated as email-glob (or @role:name);
    values validated as verb strings ⊆ {r, c}.

Tests:
  - new worm_test.go covers the embedded convention, operator-granted
    controller, w/d masking, cross-cascade union.
  - special_test.go's TestIsWormPath / TestWormFolderLevelIndex
    retired; TestWormMaskStripsWDA kept.
  - fileapi_test.go's WORM tests updated: the doc-controller grant is
    now `worm: { _doc_controller: cr }` at issued/.zddc, not
    `acl.permissions: { _doc_controller: cr }`.
  - federal-parity and admin-bypass tests unchanged — the WORM mask
    still strips w/d/a and admins still bypass.

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 08:29:11 -05:00
parent 9c7858c60a
commit 918f330a6f
12 changed files with 338 additions and 167 deletions

View file

@ -511,10 +511,11 @@ func TestFileAPI_WORM_DocControllerNeedsExplicitGrant(t *testing.T) {
t.Fatalf("dc without explicit grant → issued: want 403, got %d: %s", rec.Code, rec.Body.String()) t.Fatalf("dc without explicit grant → issued: want 403, got %d: %s", rec.Code, rec.Body.String())
} }
// Operator places an explicit grant at archive/Acme/issued/.zddc. // Operator places an explicit worm: grant at archive/Acme/issued/.zddc
// Now dc has cr at-or-below the WORM folder, which survives the mask. // naming the document-controller role. That principal then gets
issuedZ := []byte(`acl: // {r, c} inside the WORM zone — the embedded `worm: {}` (no
permissions: // controllers) is unioned with this deeper grant.
issuedZ := []byte(`worm:
_doc_controller: cr _doc_controller: cr
`) `)
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil { if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
@ -604,16 +605,16 @@ func TestFileAPI_AutoMkdirOwnership(t *testing.T) {
func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) { func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) {
_, do, root := rolePermissionsTestSetup(t) _, do, root := rolePermissionsTestSetup(t)
// Place an explicit grant so dc has cr at the issued level. // Place an explicit worm: grant so dc has cr in the issued WORM zone.
issuedZ := []byte("acl:\n permissions:\n _doc_controller: cr\n") issuedZ := []byte("worm:\n _doc_controller: cr\n")
if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil { if err := os.WriteFile(filepath.Join(root, "Project-X/archive/Acme/issued/.zddc"), issuedZ, 0o644); err != nil {
t.Fatalf("seed issued .zddc: %v", err) t.Fatalf("seed issued .zddc: %v", err)
} }
zddc.InvalidateCache(root) zddc.InvalidateCache(root)
// Doc controller mkdir under issued — should succeed (cr survives mask) // Doc controller mkdir under issued — should succeed (cr survives the
// but should NOT auto-write an ownership .zddc (issued is excluded // WORM mask) but should NOT auto-write an ownership .zddc (issued is
// from auto-own). // not declared auto_own in the cascade).
rec := do(http.MethodPost, "/Project-X/archive/Acme/issued/2026-Q2/", "dc@mycompany.com", nil, map[string]string{ rec := do(http.MethodPost, "/Project-X/archive/Acme/issued/2026-Q2/", "dc@mycompany.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir", "X-ZDDC-Op": "mkdir",
}) })

View file

@ -1300,7 +1300,7 @@ body.help-open .app-header {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span> <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-11 21:32:54 · d909756-dirty</span></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>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">

View file

@ -219,22 +219,23 @@ func (d *InternalDecider) Allow(_ context.Context, input AllowInput) (bool, erro
verb := actionVerb(input.Action) verb := actionVerb(input.Action)
email := input.User.Email email := input.User.Email
// WORM split: in Issued/Received, ancestor grants are read-only; // WORM zone: a directory whose cascade declares `worm:` (see
// only an explicit .zddc placed at-or-below the WORM folder can // defaults.zddc.yaml — archive/<party>/received and issued carry
// restore `c` (write-once) for principals it names. Admins are // `worm: {}`) is write-locked. Inside it, the effective verbs
// excluded from this code path by callers (handler package does // for a non-admin principal are:
// the IsAdmin / IsSubtreeAdmin bypass before invoking Allow).
// //
// EffectiveVerbsRange (rather than slicing chain.Levels) keeps the // (normal cascade grant & VerbR) | (worm: grant & VerbsRC)
// FULL chain visible to role-membership lookups so an ancestor's //
// role definition still applies inside the sub-range walk. // so write/delete/admin are always stripped, create survives only
if zddc.IsWormPath(input.Path) { // via the worm: map (the deployment names its document
wormIdx := zddc.WormFolderLevelIndex(input.Path, len(chain.Levels)) // controller there), and read survives via either the normal ACL
if wormIdx >= 0 { // or the worm: map. Admins are excluded from this code path by
grantAbove := zddc.EffectiveVerbsRange(chain, 0, wormIdx, email, d.Mode) & zddc.VerbR // callers — the handler does the IsAdmin / IsSubtreeAdmin bypass
grantBelow := zddc.EffectiveVerbsRange(chain, wormIdx, len(chain.Levels), email, d.Mode) & zddc.VerbsRC // before invoking Allow — so a mis-filed document still has a
return (grantAbove | grantBelow).Has(verb), nil // human escape hatch.
} if wormGrant, inWorm := zddc.WormZoneGrant(chain, email, d.Mode); inWorm {
normalRead := zddc.EffectiveVerbs(chain, email, d.Mode) & zddc.VerbR
return (normalRead | (wormGrant & zddc.VerbsRC)).Has(verb), nil
} }
return zddc.AllowedAction(chain, email, verb, d.Mode), nil return zddc.AllowedAction(chain, email, verb, d.Mode), nil

View file

@ -70,12 +70,35 @@ paths:
auto_own: true auto_own: true
# Browse shows a drag-drop overlay here. # Browse shows a drag-drop overlay here.
drop_target: true drop_target: true
# received/ and issued/ are WORM (write-once-read-many).
# The `worm:` key 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)
#
# 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:
#
# worm:
# "doc-control@example.com": cr # read + write-once
#
# worm: grants UNION across the cascade, so multiple
# controllers (or a deeper-scoped one) compose.
received: received:
default_tool: archive default_tool: archive
# received/ is WORM — express as ACL elsewhere; the worm: {}
# default convention is simply "no auto_own here".
issued: issued:
default_tool: archive default_tool: archive
worm: {}
working: working:
default_tool: mdedit default_tool: mdedit
available_tools: [mdedit, classifier] available_tools: [mdedit, classifier]

View file

@ -209,6 +209,34 @@ type ZddcFile struct {
// not its descendants. Defaults (nil): no drop zone. // not its descendants. Defaults (nil): no drop zone.
DropTarget *bool `yaml:"drop_target,omitempty" json:"drop_target,omitempty"` 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 —
// 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
//
// 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.
//
// 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"`
// AvailableTools restricts which tools the server will auto-serve // AvailableTools restricts which tools the server will auto-serve
// at this directory and its descendants. The effective list is the // at this directory and its descendants. The effective list is the
// concat-dedupe union of all AvailableTools across the cascade // concat-dedupe union of all AvailableTools across the cascade

View file

@ -229,6 +229,9 @@ func isZeroZddcFile(zf ZddcFile) bool {
if zf.AppsPubKey != "" || zf.CreatedBy != "" { if zf.AppsPubKey != "" || zf.CreatedBy != "" {
return false return false
} }
if zf.Worm != nil { // non-nil even when empty — marks a WORM zone
return false
}
if len(zf.Admins) > 0 { if len(zf.Admins) > 0 {
return false return false
} }

View file

@ -124,82 +124,15 @@ func IsAutoOwnPath(parentDir, fsRoot string) bool {
return false return false
} }
// IsWormPath reports whether requestPath crosses an // (IsWormPath / WormFolderLevelIndex retired in the cascade-config
// archive/<party>/received/ or archive/<party>/issued/ segment chain. // migration — WORM zones are declared via the `worm:` key on a
// Pure path-segment check; case-fold on canonical names. // directory's .zddc, resolved by WormZoneGrant in worm.go.
// // defaults.zddc.yaml carries `worm: {}` on archive/<party>/received
// The party segment is unrestricted — any directory under archive/ is // and archive/<party>/issued, so the canonical convention is
// treated as a party, including the self-folder. requestPath may be a // unchanged; the difference is that an operator can now mark any
// URL path ("/Project/archive/ACME/issued/foo.pdf") or a filesystem // directory WORM, or rename received/issued, without a code change.)
// path; only segment names matter.
func IsWormPath(requestPath string) bool {
parts := splitPathSegments(requestPath)
for i := 0; i+2 < len(parts); i++ {
if !strings.EqualFold(parts[i], "archive") {
continue
}
// parts[i+1] is the party name (anything).
if strings.EqualFold(parts[i+2], "received") || strings.EqualFold(parts[i+2], "issued") {
return true
}
}
return false
}
// WormMask reduces a verb set to the subset that survives the WORM // WormMask reduces a verb set to the subset that survives the WORM
// constraint: the bitwise AND with VerbsRC. Removes w, d, and a. // constraint: the bitwise AND with VerbsRC. Removes w, d, and a.
// // Generic helper kept for callers that need the masking primitive.
// Callers apply this only when IsWormPath(path) is true AND the
// principal is NOT an admin (root admin or subtree admin) — admins
// are the deliberate escape hatch for mis-filed documents.
//
// The WORM mask is split-aware via WormFolderLevelIndex: grants
// inherited from ancestors above the received/issued folder are
// masked to read only ({r}), while grants at-or-below the WORM
// folder retain {r, c} so an operator can place a .zddc at the
// received/issued folder explicitly granting `_doc_controller: cr`.
func WormMask(grant VerbSet) VerbSet { return grant & VerbsRC } func WormMask(grant VerbSet) VerbSet { return grant & VerbsRC }
// WormFolderLevelIndex returns the chain index of the deepest
// archive/<party>/(received|issued) segment in requestPath. The chain
// corresponds to the directory tree from root (index 0) to the
// requested directory; level i is the .zddc at path segment depth i.
//
// numLevels is len(chain.Levels); used to clamp results to the chain's
// actual range. URL segment i lives at chain index i+1 (root is chain
// index 0), so the WORM segment at parts[i+2] corresponds to chain
// index i+3.
//
// Returns -1 if no WORM segment is in the request path or the computed
// index is out of range. The returned index satisfies
// 0 <= index < numLevels.
func WormFolderLevelIndex(requestPath string, numLevels int) int {
if numLevels <= 0 {
return -1
}
parts := splitPathSegments(requestPath)
deepest := -1
for i := 0; i+2 < len(parts); i++ {
if !strings.EqualFold(parts[i], "archive") {
continue
}
if strings.EqualFold(parts[i+2], "received") || strings.EqualFold(parts[i+2], "issued") {
idx := i + 3
if idx < numLevels && idx > deepest {
deepest = idx
}
}
}
return deepest
}
// splitPathSegments returns the slash-separated segments of p with
// empty elements removed. Tolerates leading/trailing slashes and
// mixed separators on Windows (via filepath.ToSlash).
func splitPathSegments(p string) []string {
clean := strings.Trim(filepath.ToSlash(p), "/")
if clean == "" {
return nil
}
return strings.Split(clean, "/")
}

View file

@ -46,70 +46,11 @@ func TestIsAutoOwnPath(t *testing.T) {
} }
} }
func TestIsWormPath(t *testing.T) { // TestIsWormPath / TestWormFolderLevelIndex retired — WORM zones are
cases := map[string]bool{ // declared via the `worm:` key now (see worm.go's WormZoneGrant,
"": false, // exercised by worm_test.go). The convention (received/issued are
"/": false, // WORM) lives in defaults.zddc.yaml and is asserted via the cascade
"/Project/archive/ACME/issued": true, // lookup, not a path-segment predicate.
"/Project/archive/ACME/issued/": true,
"/Project/archive/ACME/issued/foo.pdf": true,
"/Project/archive/ACME/received/x": true,
"/Project/archive/ACME/Issued/x": true, // case-fold
"/Project/Archive/ACME/issued/x": true, // case-fold
"/Project/archive/ACME/ISSUED/x": true, // case-fold
// Per-party MDL/incoming aren't WORM.
"/Project/archive/ACME/incoming/x": false,
"/Project/archive/ACME/mdl/x": false,
// Bare "issued" or "received" not under archive/<party>/ — no WORM.
"/Project/issued/x": false,
"/Project/received/x": false,
"/Project/working/issued.md": false, // file basename, not a path segment match
"/Project/working/issued": false, // "working" is not "archive"
// Self-folder is symmetric (any party name works).
"/Project/archive/Self-Org/issued/x.pdf": true,
// Nested or deep.
"/multi/Project/archive/Vendor/received/sub/file.pdf": true,
}
for in, want := range cases {
if got := IsWormPath(in); got != want {
t.Errorf("IsWormPath(%q) = %v, want %v", in, got, want)
}
}
}
func TestWormFolderLevelIndex(t *testing.T) {
// Path /Project/archive/ACME/issued/foo.pdf
// parts: [Project, archive, ACME, issued, foo.pdf]
// issued is segment index 3, chain index 4.
if got := WormFolderLevelIndex("/Project/archive/ACME/issued/foo.pdf", 6); got != 4 {
t.Errorf("issued at depth 4: got %d, want 4", got)
}
// Same path, but the chain only has 4 levels (numLevels=4 → idx must be < 4).
if got := WormFolderLevelIndex("/Project/archive/ACME/issued/foo.pdf", 4); got != -1 {
t.Errorf("clamp: got %d, want -1", got)
}
// No WORM segment.
if got := WormFolderLevelIndex("/Project/working/foo.md", 5); got != -1 {
t.Errorf("no worm: got %d, want -1", got)
}
// Empty.
if got := WormFolderLevelIndex("", 5); got != -1 {
t.Errorf("empty: got %d, want -1", got)
}
// Nested archive/<party>/issued — deepest wins.
// parts: [P, archive, A, received, archive, B, issued, x]
// indices: 0 1 2 3 4 5 6 7
// outer match: i=1 (archive), parts[3]=received → segment idx 3, chain idx 4
// inner match: i=4 (archive), parts[6]=issued → segment idx 6, chain idx 7
// deepest = 7.
if got := WormFolderLevelIndex("/P/archive/A/received/archive/B/issued/x", 12); got != 7 {
t.Errorf("nested: got %d, want 7", got)
}
}
func TestWormMaskStripsWDA(t *testing.T) { func TestWormMaskStripsWDA(t *testing.T) {
rwcda, _ := ParseVerbSet("rwcda") rwcda, _ := ParseVerbSet("rwcda")

View file

@ -248,5 +248,34 @@ 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:") {
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),
})
}
}
return errs return errs
} }

View file

@ -78,6 +78,12 @@ func mergeOverlay(base, top ZddcFile) ZddcFile {
if top.DropTarget != nil { if top.DropTarget != nil {
out.DropTarget = top.DropTarget 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.
if top.Worm != nil {
out.Worm = mergeStringMapPreserveEmpty(out.Worm, top.Worm)
}
if top.Virtual != nil { if top.Virtual != nil {
out.Virtual = top.Virtual out.Virtual = top.Virtual
} }
@ -135,6 +141,21 @@ func mergeStringMap(base, top map[string]string) map[string]string {
return out return out
} }
// mergeStringMapPreserveEmpty is mergeStringMap 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
}
for k, v := range top {
out[k] = v
}
return out
}
func mergeStringSlice(base, top []string) []string { func mergeStringSlice(base, top []string) []string {
if len(top) == 0 { if len(top) == 0 {
return base return base

View file

@ -0,0 +1,71 @@
package zddc
// WORM (write-once-read-many) zones are declared in the cascade via
// the `worm:` key on a ZddcFile (see file.go). This file resolves the
// effective WORM grant for a principal walking a policy chain.
//
// Replaces the hardcoded IsWormPath / WormFolderLevelIndex / WormMask
// machinery (which keyed off the literal folder names "received" and
// "issued"). The convention now lives in defaults.zddc.yaml — those
// two folders carry `worm: {}` — and any operator can mark another
// directory WORM by adding `worm:` to its .zddc.
// WormZoneGrant inspects the policy chain for email. If any level in
// the chain (including paths-derived contributions) declares a `worm:`
// map, the path is inside a WORM zone: inWorm is true and grant is the
// UNION of the principal's verb grants across every Worm map in the
// chain, masked to {r, c}. When no level declares worm:, inWorm is
// false and grant is meaningless (returned as 0).
//
// Caller (the policy evaluator) combines this with the normal cascade
// read grant:
//
// if g, inWorm := WormZoneGrant(chain, email, mode); inWorm {
// effective = (normalCascadeVerbs(chain, email, mode) & VerbR) |
// (g & VerbsRC)
// return effective.Has(requestedVerb)
// }
//
// i.e. inside a WORM zone, w/d/a are always stripped; c survives only
// via the worm: grant; r survives via the normal ACL or the worm:
// 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 {
continue
}
inWorm = true
for principal, verbStr := range wm {
if !MatchesPrincipal(principal, email, chain, i, mode) {
continue
}
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
// worm: too (it doesn't today — it's declared via paths:), so
// 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
}
}
}
if !inWorm {
return 0, false
}
return grant & VerbsRC, true
}

View file

@ -0,0 +1,120 @@
package zddc
import (
"os"
"path/filepath"
"testing"
)
// TestWormZoneGrant_EmbeddedConvention — archive/<party>/received and
// 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) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
wantInWorm bool
}{
{filepath.Join(root, "Proj", "archive", "Acme", "received"), true},
{filepath.Join(root, "Proj", "archive", "Acme", "issued"), true},
{filepath.Join(root, "Proj", "archive", "Acme", "received", "2025-Q1"), true}, // deeper still WORM
{filepath.Join(root, "Proj", "archive", "Acme", "incoming"), false},
{filepath.Join(root, "Proj", "archive", "Acme", "mdl"), false},
{filepath.Join(root, "Proj", "working"), false},
{filepath.Join(root, "Proj", "staging"), false},
}
for _, tc := range cases {
chain, err := EffectivePolicy(root, tc.path)
if err != nil {
t.Fatalf("EffectivePolicy(%q): %v", tc.path, err)
}
grant, inWorm := WormZoneGrant(chain, "anyone@example.com", ModeDelegated)
if inWorm != tc.wantInWorm {
t.Errorf("WormZoneGrant(%q): inWorm = %v, want %v", tc.path[len(root):], inWorm, tc.wantInWorm)
}
if inWorm && grant != 0 {
t.Errorf("WormZoneGrant(%q): grant = %v, want 0 (embedded baseline names no controllers)", tc.path[len(root):], grant)
}
}
}
// 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.
func TestWormZoneGrant_OperatorGrantsController(t *testing.T) {
resetCache()
root := t.TempDir()
issuedDir := filepath.Join(root, "Proj", "archive", "Acme", "issued")
if err := os.MkdirAll(issuedDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, issuedDir, "worm:\n \"doc-control@example.com\": cr\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")
}
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) {
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")
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())
}
}
// TestWormZoneGrant_GrantsUnionAcrossCascade — worm: entries at
// multiple cascade levels compose: a controller named at the party
// level plus one named at the received level both get rc inside
// received/.
func TestWormZoneGrant_GrantsUnionAcrossCascade(t *testing.T) {
resetCache()
root := t.TempDir()
party := filepath.Join(root, "Proj", "archive", "Acme")
rec := filepath.Join(party, "received")
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")
chain, _ := EffectivePolicy(root, rec)
ga, inA := WormZoneGrant(chain, "alice@example.com", ModeDelegated)
if !inA || ga != VerbsRC {
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)
}
}