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>
123 lines
4 KiB
Go
123 lines
4 KiB
Go
package zddc
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsAutoOwnPath(t *testing.T) {
|
|
root := "/srv/zddc"
|
|
cases := map[string]bool{
|
|
// Project-root canonical positions.
|
|
"/srv/zddc/Project/working": true,
|
|
"/srv/zddc/Project/staging": true,
|
|
"/srv/zddc/Project/Working": true, // case-fold
|
|
"/srv/zddc/Project/STAGING": true, // case-fold
|
|
"/srv/zddc/Project/archive": false,
|
|
"/srv/zddc/Project/reviewing": false,
|
|
"/srv/zddc/Project/random": false,
|
|
|
|
// Per-party position.
|
|
"/srv/zddc/Project/archive/ACME/incoming": true,
|
|
"/srv/zddc/Project/archive/ACME/Incoming": true, // case-fold
|
|
"/srv/zddc/Project/Archive/ACME/incoming": true, // case-fold archive
|
|
"/srv/zddc/Project/archive/ACME/received": false,
|
|
"/srv/zddc/Project/archive/ACME/issued": false,
|
|
"/srv/zddc/Project/archive/ACME/mdl": false,
|
|
|
|
// Wrong depth — incoming inside something other than archive/<party>/.
|
|
"/srv/zddc/Project/working/incoming": false,
|
|
"/srv/zddc/Project/random/sub/incoming": false,
|
|
"/srv/zddc/Project/incoming": false, // depth 1 with incoming
|
|
"/srv/zddc/Project/archive/incoming": false, // depth 2
|
|
"/srv/zddc/Project/archive/ACME/incoming/sub": false, // child of incoming, not incoming itself
|
|
|
|
// Outside root.
|
|
"/elsewhere/working": false,
|
|
// Root itself or one above.
|
|
"/srv/zddc": false,
|
|
"/srv/zddc/Project": false,
|
|
}
|
|
for in, want := range cases {
|
|
if got := IsAutoOwnPath(in, root); got != want {
|
|
t.Errorf("IsAutoOwnPath(%q, %q) = %v, want %v", in, root, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIsWormPath / TestWormFolderLevelIndex retired — WORM zones are
|
|
// declared via the `worm:` key now (see worm.go's WormZoneGrant,
|
|
// exercised by worm_test.go). The convention (received/issued are
|
|
// WORM) lives in defaults.zddc.yaml and is asserted via the cascade
|
|
// lookup, not a path-segment predicate.
|
|
|
|
func TestWormMaskStripsWDA(t *testing.T) {
|
|
rwcda, _ := ParseVerbSet("rwcda")
|
|
masked := WormMask(rwcda)
|
|
if got := masked.String(); got != "rc" {
|
|
t.Errorf("WormMask(rwcda) = %q, want rc", got)
|
|
}
|
|
|
|
rw, _ := ParseVerbSet("rw")
|
|
if got := WormMask(rw).String(); got != "r" {
|
|
t.Errorf("WormMask(rw) = %q, want r", got)
|
|
}
|
|
|
|
cd, _ := ParseVerbSet("cd")
|
|
if got := WormMask(cd).String(); got != "c" {
|
|
t.Errorf("WormMask(cd) = %q, want c", got)
|
|
}
|
|
|
|
if got := WormMask(0).String(); got != "" {
|
|
t.Errorf("WormMask(0) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveCanonicalCaseFold(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(dir, "Working"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(dir, "ARCHIVE"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A regular file with a canonical name must NOT be returned (we only resolve directories).
|
|
if err := os.WriteFile(filepath.Join(dir, "staging"), []byte{}, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cases := map[string]string{
|
|
"working": "Working", // PascalCase wins because it exists on disk
|
|
"WORKING": "Working",
|
|
"Working": "Working",
|
|
"archive": "ARCHIVE",
|
|
"reviewing": "", // not present
|
|
"staging": "", // present as a file, not a directory — must skip
|
|
}
|
|
for logical, want := range cases {
|
|
got, err := ResolveCanonical(dir, logical)
|
|
if err != nil {
|
|
t.Errorf("ResolveCanonical(%q): %v", logical, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("ResolveCanonical(%q) = %q, want %q", logical, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveCanonicalMissingParent(t *testing.T) {
|
|
got, err := ResolveCanonical(filepath.Join(t.TempDir(), "does-not-exist"), "working")
|
|
if err != nil {
|
|
t.Errorf("expected nil error for missing parent, got %v", err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("expected empty result for missing parent, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestCanonicalLists and TestIsProjectRootFolder retired in Phase 3 —
|
|
// the canonical convention is now expressed in defaults.zddc.yaml and
|
|
// asserted by lookups_test.go (TestDefaultToolAt_FromEmbeddedConvention,
|
|
// TestIsDeclaredPath_FromEmbeddedConvention, etc.).
|