feat(zddc): operator .zddc.zip mountable at any cascade level (migration phase 5)
EffectivePolicy now reads, at every directory in the walk, an optional <dir>/.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) <noreply@anthropic.com>
This commit is contained in:
parent
21f6883157
commit
4681f2c358
2 changed files with 127 additions and 12 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package zddc
|
package zddc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -126,24 +127,28 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) {
|
||||||
return cached.(PolicyChain), nil
|
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 <dir>/.zddc overlaid on top
|
||||||
|
// (most-specific human edit wins). Either, both, or neither may be present.
|
||||||
onDisk := make([]ZddcFile, 0, len(dirs))
|
onDisk := make([]ZddcFile, 0, len(dirs))
|
||||||
hasAny := false
|
hasAny := false
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
zddcPath := filepath.Join(dir, ".zddc")
|
level := ZddcFile{}
|
||||||
_, err := os.Stat(zddcPath)
|
if zipZf, ok := zipPolicyAt(dir); ok {
|
||||||
if err == nil {
|
|
||||||
hasAny = true
|
hasAny = true
|
||||||
parsed, perr := ParseFile(zddcPath)
|
level = zipZf
|
||||||
if perr != nil {
|
|
||||||
onDisk = append(onDisk, ZddcFile{})
|
|
||||||
} else {
|
|
||||||
onDisk = append(onDisk, parsed)
|
|
||||||
}
|
}
|
||||||
} else {
|
zddcPath := filepath.Join(dir, ".zddc")
|
||||||
onDisk = append(onDisk, ZddcFile{})
|
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
|
// Walk ancestor paths: trees alongside the on-disk chain. Each
|
||||||
// virtual source is a paths-map seeded by an ancestor's Paths
|
// virtual source is a paths-map seeded by an ancestor's Paths
|
||||||
|
|
@ -254,6 +259,31 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) {
|
||||||
return chain, nil
|
return chain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zipPolicyAt loads an operator policy bundle at <dir>/.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
|
// EffectiveFieldCodes returns the merged field-code vocabulary
|
||||||
// visible at the leaf of this chain. Walks root → leaf, applying
|
// visible at the leaf of this chain. Walks root → leaf, applying
|
||||||
// map-merge per top-level key (a leaf entry for the same code
|
// map-merge per top-level key (a leaf entry for the same code
|
||||||
|
|
|
||||||
85
zddc/internal/zddc/cascade_zip_test.go
Normal file
85
zddc/internal/zddc/cascade_zip_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue