diff --git a/zddc/README.md b/zddc/README.md index 29b2255..9777344 100644 --- a/zddc/README.md +++ b/zddc/README.md @@ -251,9 +251,15 @@ for a level whose `acl.permissions` map matches the user. - No `.zddc` anywhere (`HasAnyFile=false`) → **allow** (the empty-tree default). - At least one `.zddc` existed (`HasAnyFile=true`) → **403 Forbidden** (default-deny). +The walk respects an **inherit fence** (see "The `inherit:` directive" below). +A level whose `acl.inherit: false` flag is set acts as a fence: ancestors above +it are invisible to descendants at-and-below the fence, both for grants and for +role lookups. In strict cascade mode the fence is ignored (NIST AC-6 invariant). + Implementation: `GrantedVerbsAtLevel` (`zddc/internal/zddc/acl.go`) computes the per-level grant; `EffectiveVerbs` / `AllowedAction` walk the chain; the chain -itself is built by `EffectivePolicy` (`zddc/internal/zddc/cascade.go`). +itself is built by `EffectivePolicy` (`zddc/internal/zddc/cascade.go`); the +fence is computed by `PolicyChain.VisibleStart`. #### Cascade mode @@ -272,6 +278,56 @@ root-admin involvement. Federal deployments needing absolute parent denies The mode is logged at startup and surfaced on `/.profile/config`. Subtree `.zddc` files cannot change the mode — it's a deployment-wide policy. +#### The `inherit:` directive + +A `.zddc` may declare `acl.inherit: false` to fence off all ancestor grants +and roles from the descendant subtree. Useful for the "complete reset, then +add back specific principals" pattern — vendor folders, regulated subtrees, +anywhere a permissive ancestor rule is too broad and the operator wants +explicit local control. + +```yaml +# /Vendor/.zddc — vendor folder, fully reset +acl: + inherit: false # ancestors above this level become invisible + permissions: + "*@vendor.com": rwcd # the vendor and the doc controller are + _doc_controller: rwcda # the only principals with access here +``` + +Behaviour: + +- **Grants:** the cascade walker (leaf → root) stops at the deepest + `inherit: false` level when looking for a matching grant. Ancestor + `permissions` entries don't contribute. The default-deny rule still + applies if no descendant grant matches. +- **Roles:** role definitions in ancestors above the fence are also + invisible. To use a role inside a fenced subtree, redefine it locally + (a redefinition at-or-below the fence is fine). +- **Admins:** the root `admins:` list is unaffected. Root admins still + bypass all ACL evaluation, fence or no fence — that's the deliberate + escape hatch for misfiled documents. +- **WORM:** the `archive//issued|received/` mask is path-based, + not cascade-based. `inherit:` does not change WORM behaviour. + +**Strict cascade mode IGNORES `inherit: false`.** NIST AC-6 requires +ancestor explicit-denies to be absolute, and the inherit directive +would let a leaf widen access an ancestor refused. Under +`--cascade-mode=strict` the directive has no effect (and the bundled +federal Rego at `--print-rego=federal` mirrors that rule). Operators +who need fence-style "reset" semantics in a federal-track deployment +should not use the directive — instead, restructure the tree so the +permissive ancestor rule never appears. + +The cascade tracer (`/.profile/effective-policy`) surfaces every +level's `inherit` flag and the `chain.visible_start` index so a +reviewer can see exactly where the fence sits and whether it's in +effect under the active mode. + +Implementation: parser (`zddc/internal/zddc/file.go`), +`PolicyChain.VisibleStart` (`zddc/internal/zddc/cascade.go`), and the +fence-aware role walk (`zddc/internal/zddc/roles.go`). + #### Special folders Five folder names trigger built-in behaviors regardless of cascade mode (canonical list in `zddc/internal/zddc/special.go`): @@ -401,14 +457,21 @@ naive intuition suggests. 1. **Same-level `allow + deny "*@company.com"` does NOT lock the level down to the allow's targets.** Deny is checked before allow within a single `.zddc`, so the allowed user's email matches the deny first and is blocked. To exclude insiders - from a leaf, use the two-level gate-and-reallow (parent denies, deeper level - re-allows) — *or* avoid putting `*@company.com` at any ancestor. + from a leaf, use the `inherit: false` directive (preferred — see "The + `inherit:` directive" above), or the two-level gate-and-reallow pattern, or + avoid putting `*@company.com` at any ancestor in the first place. ```yaml # /Closed/.zddc — DOES NOT WORK as intended acl: allow: [alice@company.com] deny: ["*@company.com"] # blocks alice too — deny is checked first at same level + + # /Closed/.zddc — works correctly + acl: + inherit: false # ancestor "*@company.com" rule is invisible here + permissions: + alice@company.com: rwcd ``` 2. **A leaf-level `allow: [subset]` does NOT restrict when a parent has