Replaces the binary acl.allow/deny model with five permission verbs
(r/w/c/d/a) and first-class roles, and adds an authenticated file API
(PUT/DELETE/POST move/mkdir) so the HTML tools can edit-in-place over
HTTP. Closes the AC-3(7) and AC-6 federal-readiness gaps.
File API (zddc/internal/handler/fileapi.go)
- PUT <new> → action c
- PUT <existing> → action w
- PUT <.zddc> → action a (CanEditZddc strict-ancestor rule)
- DELETE → action d
- POST mkdir → action c (auto-writes creator-owned .zddc when the
parent is Incoming/Working/Staging)
- POST move → action w on src + c on dst, atomic via os.Rename
- Optional If-Match for optimistic concurrency, --max-write-bytes cap,
audit log emits a structured file_write event per operation.
Permission model (zddc/internal/zddc/{acl,file,roles,cascade_mode}.go)
- acl.permissions: { principal → verb-set } map; principals are email
patterns or role names. Empty verb set is an explicit deny.
- roles: { name → members } definitions, available at the level they
declare and all descendants. Closer-to-leaf shadows ancestor.
- Legacy acl.allow/deny still work; they fold into permissions at
parse time (allow → "rwcd", deny → "").
- Cascade walks leaf→root; first level with any matching entry wins;
the union of matching verb sets at that level decides.
- --cascade-mode=strict adds a root→leaf ancestor-deny pre-pass so an
ancestor explicit-deny is absolute (NIST AC-6). Default delegated
preserves the existing commercial behavior.
Special folders (zddc/internal/zddc/special.go)
- Incoming / Working / Staging: mkdir auto-writes a .zddc into the new
subdir granting created_by + that email rwcda directly. Same form
operators write by hand; creator can edit it later to add others.
- Issued / Received: server-enforced WORM split. Cascade grants
inherited from above the WORM folder are masked to r only; grants
placed at-or-below the WORM folder retain r,c. Operators grant
write-once (cr) to the doc controller via an explicit .zddc at the
Issued/Received folder. Admins exempt — only escape hatch.
Browser polyfill (shared/zddc-source.js)
- HttpDirectoryHandle + HttpFileHandle implement the FS Access API
surface (values, getFileHandle, createWritable, removeEntry,
queryPermission/requestPermission) over zddc-server's listing JSON
and file API. Existing tools written against showDirectoryPicker
work unchanged.
- detectServerRoot() returns { handle, status }: tools auto-load on
HTTP, surface a clear "no permission to list" message on 403, and
fall back to the welcome screen on 0.
- classifier renames take the atomic POST move path on HTTP-backed
handles; mdedit and transmittal route reads/writes through the
polyfill so prior FS-API code paths cover both modes.
Tests
- zddc/internal/zddc/{cascade_mode,roles,special,acl}_test.go cover
delegated vs strict, role membership / shadowing / legacy fallback,
WORM split semantics, verb-set parser round-trip.
- zddc/internal/handler/fileapi_test.go now also covers role-based
vendor scenarios, WORM blocking vendor & doc controller writes,
explicit Issued .zddc unlocking the cr drop-box, admin bypass,
auto-ownership on mkdir, and strict-mode lockouts.
Docs
- ARCHITECTURE.md + zddc/README.md document the verb model, role
syntax, special-folder behaviors, cascade-mode flag, and full file
API surface. Federal-readiness gap analysis strikes AC-3(7) and
AC-6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
1.6 KiB
Go
48 lines
1.6 KiB
Go
package zddc
|
|
|
|
// CascadeMode selects the access-decision algorithm used by AllowedAction.
|
|
//
|
|
// ModeDelegated (default) preserves the historical commercial-tenant
|
|
// behavior: the cascade walks leaf→root and the first level with a
|
|
// matching entry decides. Subtree allows can override ancestor denies —
|
|
// this is the load-bearing delegation primitive that lets a subtree
|
|
// owner grant access without root-admin involvement.
|
|
//
|
|
// ModeStrict implements the federal posture (NIST AC-6 "least
|
|
// privilege"): a deny anywhere in the ancestor chain is absolute and
|
|
// cannot be overridden by a leaf grant. Implemented as a two-pass
|
|
// evaluation — first walk root→leaf for any matching explicit deny,
|
|
// then walk leaf→root for the grant.
|
|
//
|
|
// The mode is operator-controlled at startup via --cascade-mode (config
|
|
// flag) or ZDDC_CASCADE_MODE (env var). Subtree .zddc files cannot
|
|
// override the mode — it is a deployment-wide policy.
|
|
type CascadeMode int
|
|
|
|
const (
|
|
ModeDelegated CascadeMode = iota
|
|
ModeStrict
|
|
)
|
|
|
|
// String returns the operator-facing name (matches the flag value).
|
|
func (m CascadeMode) String() string {
|
|
switch m {
|
|
case ModeStrict:
|
|
return "strict"
|
|
default:
|
|
return "delegated"
|
|
}
|
|
}
|
|
|
|
// ParseCascadeMode resolves a flag/env string to a CascadeMode. Empty
|
|
// or unrecognized input defaults to ModeDelegated; the caller can warn
|
|
// on unrecognized values, but the safe default is the existing behavior.
|
|
func ParseCascadeMode(s string) (CascadeMode, bool) {
|
|
switch s {
|
|
case "", "delegated":
|
|
return ModeDelegated, true
|
|
case "strict":
|
|
return ModeStrict, true
|
|
}
|
|
return ModeDelegated, false
|
|
}
|