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:
parent
8ffbcb90d1
commit
ee50213e0b
1 changed files with 66 additions and 3 deletions
|
|
@ -251,9 +251,15 @@ for a level whose `acl.permissions` map matches the user.
|
||||||
- No `.zddc` anywhere (`HasAnyFile=false`) → **allow** (the empty-tree default).
|
- No `.zddc` anywhere (`HasAnyFile=false`) → **allow** (the empty-tree default).
|
||||||
- At least one `.zddc` existed (`HasAnyFile=true`) → **403 Forbidden** (default-deny).
|
- 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
|
Implementation: `GrantedVerbsAtLevel` (`zddc/internal/zddc/acl.go`) computes the
|
||||||
per-level grant; `EffectiveVerbs` / `AllowedAction` walk the chain; the chain
|
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
|
#### 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
|
The mode is logged at startup and surfaced on `/.profile/config`. Subtree
|
||||||
`.zddc` files cannot change the mode — it's a deployment-wide policy.
|
`.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
|
#### Special folders
|
||||||
|
|
||||||
Five folder names trigger built-in behaviors regardless of cascade mode (canonical list in `zddc/internal/zddc/special.go`):
|
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
|
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
|
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
|
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
|
from a leaf, use the `inherit: false` directive (preferred — see "The
|
||||||
re-allows) — *or* avoid putting `*@company.com` at any ancestor.
|
`inherit:` directive" above), or the two-level gate-and-reallow pattern, or
|
||||||
|
avoid putting `*@company.com` at any ancestor in the first place.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# /Closed/.zddc — DOES NOT WORK as intended
|
# /Closed/.zddc — DOES NOT WORK as intended
|
||||||
acl:
|
acl:
|
||||||
allow: [alice@company.com]
|
allow: [alice@company.com]
|
||||||
deny: ["*@company.com"] # blocks alice too — deny is checked first at same level
|
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
|
2. **A leaf-level `allow: [subset]` does NOT restrict when a parent has
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue