docs(zddc): document the inherit: directive in the access-control ref

Add a new "The inherit: directive" subsection in zddc/README.md
under "Access control: the .zddc cascade" describing the directive's
purpose (vendor-folder reset, regulated subtrees), the four behaviour
points (grants, roles, admins, WORM), the strict-mode refusal under
NIST AC-6, and the tracer's visibility. Cross-link from the "How a
request is evaluated" walkthrough so a reader who's looking up the
core walk can find the fence behaviour without having to scan further.

Update the "Patterns that look secure but aren't" trap #1 (same-level
allow + deny "*@company.com") to recommend inherit: false as the
preferred fix, with a worked-example .zddc snippet alongside the
broken one. The two-level gate-and-reallow pattern remains as a
fallback for federal-track deployments where inherit: is refused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-07 11:04:38 -05:00
parent 8ffbcb90d1
commit ee50213e0b

View file

@ -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
# <ZDDC_ROOT>/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/<party>/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