From 4681f2c358dcbe7e3f2f683cc0398f8f6ae43029 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Fri, 5 Jun 2026 11:29:12 -0500 Subject: [PATCH] feat(zddc): operator .zddc.zip mountable at any cascade level (migration phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EffectivePolicy now reads, at every directory in the walk, an optional /.zddc.zip policy bundle: its members are loaded into a PolicyTree, Assemble()d into a nested ZddcFile, and merged UNDER the dir's on-disk .zddc (most-specific human edit wins). Because Assemble produces an ordinary paths:-bearing ZddcFile, the existing walker threads the bundle's deeper members to descendants and honors inherit:false with zero new cascade logic — the bundle is just another per-level policy source. So a .zddc.zip dropped at ANY directory mounts a policy subtree there; combined with inherit:false + acl.inherit:false in its root member it's a self-contained island that ignores the site defaults (do-something-completely-different). Member paths use "*" wildcards, resolved by the same literal-first matching as paths:. A tool-HTML-only bundle (no .zddc members) contributes no policy. Test: a bundle at /Proj/special grants only *@vendor.com (rwcd at the mount, r at "*" descendants) and, fenced, blocks the embedded project_team grant that still applies outside the island. Co-Authored-By: Claude Opus 4.8 (1M context) --- zddc/internal/zddc/cascade.go | 54 ++++++++++++---- zddc/internal/zddc/cascade_zip_test.go | 85 ++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 zddc/internal/zddc/cascade_zip_test.go diff --git a/zddc/internal/zddc/cascade.go b/zddc/internal/zddc/cascade.go index cd2bb78..1e32bbe 100644 --- a/zddc/internal/zddc/cascade.go +++ b/zddc/internal/zddc/cascade.go @@ -1,6 +1,7 @@ package zddc import ( + "archive/zip" "os" "path/filepath" "strings" @@ -126,23 +127,27 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) { return cached.(PolicyChain), nil } - // Build policy chain: read each on-disk .zddc file. + // Build policy chain: read each level's on-disk policy. A level's + // contribution is an optional .zddc.zip policy bundle mounted here (a whole + // subtree: its own-level member at this level, its deeper members threaded + // to descendants via Paths) with the plain /.zddc overlaid on top + // (most-specific human edit wins). Either, both, or neither may be present. onDisk := make([]ZddcFile, 0, len(dirs)) hasAny := false for _, dir := range dirs { - zddcPath := filepath.Join(dir, ".zddc") - _, err := os.Stat(zddcPath) - if err == nil { + level := ZddcFile{} + if zipZf, ok := zipPolicyAt(dir); ok { hasAny = true - parsed, perr := ParseFile(zddcPath) - if perr != nil { - onDisk = append(onDisk, ZddcFile{}) - } else { - onDisk = append(onDisk, parsed) - } - } else { - onDisk = append(onDisk, ZddcFile{}) + level = zipZf } + zddcPath := filepath.Join(dir, ".zddc") + if _, err := os.Stat(zddcPath); err == nil { + hasAny = true + if parsed, perr := ParseFile(zddcPath); perr == nil { + level = mergeOverlay(level, parsed) + } + } + onDisk = append(onDisk, level) } // Walk ancestor paths: trees alongside the on-disk chain. Each @@ -254,6 +259,31 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) { return chain, nil } +// zipPolicyAt loads an operator policy bundle at /.zddc.zip and assembles +// it into a single nested ZddcFile (its own-level content + Paths threading its +// deeper members to descendants), or (_, false) when the bundle is absent, +// unreadable, or carries no .zddc members (e.g. a tool-HTML-only bundle — those +// are ignored for policy). Mounting the bundle at dir contributes a policy +// subtree there; inherit:false in its resolved .zddc makes that subtree a +// self-contained island. Member paths use "*" for the any-segment wildcard, +// resolved by the same literal-first matching as paths:. +func zipPolicyAt(dir string) (ZddcFile, bool) { + zipPath := filepath.Join(dir, ".zddc.zip") + if fi, err := os.Stat(zipPath); err != nil || fi.IsDir() { + return ZddcFile{}, false + } + zr, err := zip.OpenReader(zipPath) + if err != nil { + return ZddcFile{}, false + } + defer zr.Close() + tree, err := LoadPolicyTreeFromFS(zr, ".") + if err != nil || len(tree) == 0 { + return ZddcFile{}, false + } + return tree.Assemble(), true +} + // EffectiveFieldCodes returns the merged field-code vocabulary // visible at the leaf of this chain. Walks root → leaf, applying // map-merge per top-level key (a leaf entry for the same code diff --git a/zddc/internal/zddc/cascade_zip_test.go b/zddc/internal/zddc/cascade_zip_test.go new file mode 100644 index 0000000..27ec09b --- /dev/null +++ b/zddc/internal/zddc/cascade_zip_test.go @@ -0,0 +1,85 @@ +package zddc + +import ( + "archive/zip" + "os" + "path/filepath" + "testing" +) + +func writeTestZip(t *testing.T, zipPath string, members map[string]string) { + t.Helper() + f, err := os.Create(zipPath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + zw := zip.NewWriter(f) + for name, body := range members { + w, err := zw.Create(name) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write([]byte(body)); err != nil { + t.Fatal(err) + } + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } +} + +// A .zddc.zip dropped at any directory mounts a policy subtree there: its +// own-level member governs that directory, its "*"/named members govern +// descendants, and inherit:false makes it a self-contained island that ignores +// the ancestor cascade + embedded site defaults. +func TestZddcZipMountedAtSubtree(t *testing.T) { + root := t.TempDir() + // Site root: project_team has a member; embedded defaults apply below. + if err := os.WriteFile(filepath.Join(root, ".zddc"), + []byte("roles:\n project_team:\n members: [team@x]\n"), 0o644); err != nil { + t.Fatal(err) + } + for _, d := range []string{"Proj/special", "Proj/normal"} { + if err := os.MkdirAll(filepath.Join(root, filepath.FromSlash(d)), 0o755); err != nil { + t.Fatal(err) + } + } + + // Drop a self-contained island at /Proj/special (inherit:false) granting + // only *@vendor.com, with a "*" descendant rule (read-only below). + // A complete island fences both layers: top-level inherit:false drops the + // embedded defaults + ancestor paths: contributions, and acl.inherit:false + // clamps the ACL level-walk so ancestor levels' grants don't leak in. + writeTestZip(t, filepath.Join(root, "Proj", "special", ".zddc.zip"), map[string]string{ + ".zddc": "inherit: false\nacl:\n inherit: false\n permissions:\n \"*@vendor.com\": rwcd\n", + "*/.zddc": "acl:\n permissions:\n \"*@vendor.com\": r\n", + }) + InvalidateCache(root) + + verbs := func(dir, email string) VerbSet { + chain, err := EffectivePolicy(root, filepath.Join(root, filepath.FromSlash(dir))) + if err != nil { + t.Fatalf("EffectivePolicy %s: %v", dir, err) + } + return EffectiveVerbs(chain, email) + } + + // Bundle root member governs /Proj/special. + if v := verbs("Proj/special", "u@vendor.com"); !v.Has(VerbC) || !v.Has(VerbW) || !v.Has(VerbD) { + t.Errorf("/Proj/special vendor verbs = %v, want rwcd", v) + } + // Bundle's */.zddc governs a (virtual) descendant — read-only, deepest-wins. + if v := verbs("Proj/special/anychild", "u@vendor.com"); !v.Has(VerbR) || v.Has(VerbW) { + t.Errorf("/Proj/special/anychild vendor verbs = %v, want r only", v) + } + // inherit:false fences the site defaults: the embedded project-level + // project_team grant has NO effect inside the island. + if v := verbs("Proj/special", "team@x"); v != 0 { + t.Errorf("/Proj/special team verbs = %v, want none (fenced island)", v) + } + // Outside the island, the embedded project-level grant still applies. + if v := verbs("Proj/normal", "team@x"); !v.Has(VerbR) { + t.Errorf("/Proj/normal team verbs = %v, want r (embedded project_team:r)", v) + } +}