docs: fix stale "fenced/private home" claims — default homes are shared
The auto_own_fenced mechanism (private per-creator home via inherit:false) still exists, but the current default tree sets it NOWHERE — the working/staging/ incoming/reviewing <party> homes are auto_own but UNFENCED, so ancestor grants (project_team: cr at working/) cascade in and they are shared team folders. Code comments (file.go AutoOwnFenced, special.go WriteAutoOwnZddcFenced, ensure.go, fileapi.go) and AGENTS.md (role model + the auto_own_fenced key) still described per-user homes as fenced/private-by-default — a pre-reshape artifact. Correct them: fencing is an opt-in not used by the default tree; the party homes are unfenced/shared. No behavior change (grep finds no auto_own_fenced in internal/zddc/defaults). From the deferred-findings triage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
88ef2dd921
commit
84c1b58b66
5 changed files with 38 additions and 32 deletions
|
|
@ -526,12 +526,12 @@ The in-flight lifecycle slots form a one-way ratchet:
|
||||||
|
|
||||||
`working/` → `staging/` → `issued/` (WORM)
|
`working/` → `staging/` → `issued/` (WORM)
|
||||||
|
|
||||||
Each handoff drops `project_team`'s modify rights for the slot they pushed from. At `working/` they have `cr` plus `rwcda` inside their auto-own-fenced `<email>/` home. At `staging/` they have `cr` only (drop files, no modify after). DC takes over with `rwcd` at staging and files to `issued/` via the WORM `cr` grant. Same shape on the inbound side via `incoming/` → `received/` (WORM).
|
Each handoff drops `project_team`'s modify rights for the slot they pushed from. At `working/` they have `cr` plus `rwcda` inside the `<party>/` folder they create (auto-owned but **unfenced** — `working/` is a shared team space, so peers keep their `cr` there). At `staging/` they have `cr` only (drop files, no modify after). DC takes over with `rwcd` at staging and files to `issued/` via the WORM `cr` grant. Same shape on the inbound side via `incoming/` → `received/` (WORM).
|
||||||
|
|
||||||
Pick a role per persona:
|
Pick a role per persona:
|
||||||
|
|
||||||
- `document_controller` — per-party records custodian; files into WORM `received/issued`, manages the `working/staging/reviewing` lifecycle, QCs the counterparty's drops in `incoming/`. **NOT a subtree-admin** anywhere — authority comes purely from cascade grants (the role-level `rwcda` written by `auto_own_roles` at each party, plus explicit `rwcd` at `incoming/` and `staging/`). They cannot bypass WORM (only worm-create via the list) or reach inside fenced working homes.
|
- `document_controller` — per-party records custodian; files into WORM `received/issued`, manages the `working/staging/reviewing` lifecycle, QCs the counterparty's drops in `incoming/`. **NOT a subtree-admin** anywhere — authority comes purely from cascade grants (the role-level `rwcda` written by `auto_own_roles` at each party, plus explicit `rwcd` at `incoming/` and `staging/`). They cannot bypass WORM (only worm-create via the list).
|
||||||
- `project_team` — day-to-day contributor. Reads across the project; ratchets through the in-flight slots. Owns their `archive/<party>/working/<email>/` home via auto-own with a fenced `.zddc` (`inherit: false`).
|
- `project_team` — day-to-day contributor. Reads across the project; ratchets through the in-flight slots. Auto-owns (`rwcda`) the `working/<party>/` folder they create; it is **unfenced**, so `working/` stays a shared team space (every `project_team` member keeps cascade `cr` there). A per-directory `auto_own_fenced` opt-in (not set in the default tree) would make it private.
|
||||||
- `observer` — pure read-only across the project. No auto-own home (the role itself has no `c` anywhere). Intended for auditors, regulators, and external read-only viewers who must not contribute content.
|
- `observer` — pure read-only across the project. No auto-own home (the role itself has no `c` anywhere). Intended for auditors, regulators, and external read-only viewers who must not contribute content.
|
||||||
|
|
||||||
**Roles overlap on purpose.** DCs are typically internal employees and ARE in `project_team` (often defined as `*@example.com`). The cascade is "deepest level that has any matching principal wins for that level, with within-level UNION of all matched principals". To prevent a `project_team: cr` grant at the slot from shadowing a DC's role-level `rwcda` inherited from the party folder, the embedded defaults RESTATE `document_controller: rwcda` at every slot that has a project_team-specific grant (`working/`, `staging/`, `reviewing/`). Within-level union → DC gets `rwcda` ∪ `cr` = `rwcda`. Operators adding new slot-level project_team grants in their own `.zddc` files should follow the same pattern. (Internal `observer` users matched by the project_team wildcard would still be lifted to `cr` by the union — observer is intended for EXTERNAL auditors whose emails don't match the wildcard. Deployments with internal observers should use explicit project_team membership instead of a wildcard.)
|
**Roles overlap on purpose.** DCs are typically internal employees and ARE in `project_team` (often defined as `*@example.com`). The cascade is "deepest level that has any matching principal wins for that level, with within-level UNION of all matched principals". To prevent a `project_team: cr` grant at the slot from shadowing a DC's role-level `rwcda` inherited from the party folder, the embedded defaults RESTATE `document_controller: rwcda` at every slot that has a project_team-specific grant (`working/`, `staging/`, `reviewing/`). Within-level union → DC gets `rwcda` ∪ `cr` = `rwcda`. Operators adding new slot-level project_team grants in their own `.zddc` files should follow the same pattern. (Internal `observer` users matched by the project_team wildcard would still be lifted to `cr` by the union — observer is intended for EXTERNAL auditors whose emails don't match the wildcard. Deployments with internal observers should use explicit project_team membership instead of a wildcard.)
|
||||||
|
|
@ -544,7 +544,7 @@ Pick a role per persona:
|
||||||
- `roles: { <name>: { members: [...], reset: <bool>? } }` — members union across the cascade unless `reset: true`.
|
- `roles: { <name>: { members: [...], reset: <bool>? } }` — members union across the cascade unless `reset: true`.
|
||||||
- `admins: [<email>, ...]` — root only; sudo-style elevation per request.
|
- `admins: [<email>, ...]` — root only; sudo-style elevation per request.
|
||||||
- `auto_own: <bool>` — when true, ensure.go writes a `.zddc` granting the creator's email `rwcda` on first mkdir.
|
- `auto_own: <bool>` — when true, ensure.go writes a `.zddc` granting the creator's email `rwcda` on first mkdir.
|
||||||
- `auto_own_fenced: <bool>` — adds `inherit: false` to the auto-own `.zddc` (private-by-default home). No effect without `auto_own: true`.
|
- `auto_own_fenced: <bool>` — adds `inherit: false` to the auto-own `.zddc`, making the directory private to its creator (ancestor grants don't cascade in). **Opt-in — not set anywhere in the default tree**, so the default working/staging/incoming/reviewing party homes are unfenced/shared. No effect without `auto_own: true`.
|
||||||
- `auto_own_roles: [<role>, ...]` — additional role names that get `rwcda` in the auto-own `.zddc`, alongside the creator's email. Lets the schema express role-level peer authority without `admins:` (which would be subtree-admin and bypass WORM/fences via elevation).
|
- `auto_own_roles: [<role>, ...]` — additional role names that get `rwcda` in the auto-own `.zddc`, alongside the creator's email. Lets the schema express role-level peer authority without `admins:` (which would be subtree-admin and bypass WORM/fences via elevation).
|
||||||
- `title:` — read only from the per-project `.zddc`; surfaces on the landing-page picker.
|
- `title:` — read only from the per-project `.zddc`; surfaces on the landing-page picker.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -821,10 +821,12 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
// - abs's parent is declared auto_own — every child mkdir under
|
// - abs's parent is declared auto_own — every child mkdir under
|
||||||
// an auto-own folder (working/, staging/, archive/<party>/,
|
// an auto-own folder (working/, staging/, archive/<party>/,
|
||||||
// archive/<party>/incoming/, …) gets the creator's grant.
|
// archive/<party>/incoming/, …) gets the creator's grant.
|
||||||
// The fence (inherit:false) follows abs's own cascade level:
|
// The fence (inherit:false) follows abs's own cascade level via
|
||||||
// per-user homes under working/ declare auto_own_fenced, so the
|
// AutoOwnFencedAt. It is an opt-in the default tree does not set —
|
||||||
// generated .zddc is private; other auto-own positions are
|
// the working/staging/incoming/reviewing party homes are auto-owned
|
||||||
// unfenced so ancestor grants still cascade through.
|
// but UNFENCED, so ancestor grants (e.g. project_team cr) cascade
|
||||||
|
// through and they behave as shared team folders. An operator can
|
||||||
|
// set auto_own_fenced on a position to make it private.
|
||||||
if email != "" {
|
if email != "" {
|
||||||
if zddc.AutoOwnAt(cfg.Root, abs) || zddc.AutoOwnAt(cfg.Root, filepath.Dir(abs)) {
|
if zddc.AutoOwnAt(cfg.Root, abs) || zddc.AutoOwnAt(cfg.Root, filepath.Dir(abs)) {
|
||||||
roles := zddc.AutoOwnRolesAt(cfg.Root, abs)
|
roles := zddc.AutoOwnRolesAt(cfg.Root, abs)
|
||||||
|
|
|
||||||
|
|
@ -200,10 +200,11 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil
|
||||||
|
|
||||||
// Determine if this newly-created ancestor is an auto-own
|
// Determine if this newly-created ancestor is an auto-own
|
||||||
// position and whether it should be fenced (inherit: false).
|
// position and whether it should be fenced (inherit: false).
|
||||||
// Resolved via the .zddc cascade — internal/zddc/defaults/
|
// Resolved via the .zddc cascade — the embedded defaults
|
||||||
// carries the canonical "working/staging auto-own + per-user
|
// (internal/zddc/defaults/) declare auto_own at the working/
|
||||||
// homes fenced + incoming auto-own" convention, and any
|
// staging/ incoming/ reviewing/ <party> homes but do NOT fence
|
||||||
// on-disk .zddc can override per-directory.
|
// them (they are shared team folders); an on-disk .zddc can opt
|
||||||
|
// a directory into fencing per-directory with auto_own_fenced.
|
||||||
_ = parentSegs // depth-tracking no longer needed
|
_ = parentSegs // depth-tracking no longer needed
|
||||||
_ = i
|
_ = i
|
||||||
autoOwn := AutoOwnAt(fsRoot, pathSoFar)
|
autoOwn := AutoOwnAt(fsRoot, pathSoFar)
|
||||||
|
|
@ -222,12 +223,11 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil
|
||||||
|
|
||||||
// Seed auto-own .zddc on the canonical positions that were freshly
|
// Seed auto-own .zddc on the canonical positions that were freshly
|
||||||
// created. Skip if no principal email is available (anonymous or
|
// created. Skip if no principal email is available (anonymous or
|
||||||
// system writes). The fenced variant is used at per-user home
|
// system writes). The fenced variant (inherit:false, private to the
|
||||||
// folders under working/ — private by default; owner can later
|
// creator) is an opt-in the default tree does not use — see
|
||||||
// edit the .zddc to add collaborators. Role grants (from the
|
// AutoOwnFencedAt. Role grants (from the cascade's auto_own_roles
|
||||||
// cascade's auto_own_roles list) are written alongside the
|
// list) are written alongside the creator email so role-level peer
|
||||||
// creator email so role-level peer authority survives without
|
// authority survives without needing a subtree-admin grant.
|
||||||
// needing a subtree-admin grant.
|
|
||||||
if principalEmail != "" {
|
if principalEmail != "" {
|
||||||
for _, c := range freshlyCreated {
|
for _, c := range freshlyCreated {
|
||||||
if !c.autoOwn {
|
if !c.autoOwn {
|
||||||
|
|
|
||||||
|
|
@ -228,12 +228,15 @@ type ZddcFile struct {
|
||||||
// created. Empty (nil) inherits via cascade.
|
// created. Empty (nil) inherits via cascade.
|
||||||
AutoOwn *bool `yaml:"auto_own,omitempty" json:"auto_own,omitempty"`
|
AutoOwn *bool `yaml:"auto_own,omitempty" json:"auto_own,omitempty"`
|
||||||
|
|
||||||
// AutoOwnFenced augments AutoOwn: when true, the generated .zddc
|
// AutoOwnFenced augments AutoOwn: when true, the generated .zddc is
|
||||||
// is written with `inherit: false` so the new directory is
|
// written with `inherit: false` so the new directory is private to its
|
||||||
// private to its creator (ancestor ACL grants don't apply). Used
|
// creator (ancestor ACL grants don't cascade in). It is an OPT-IN an
|
||||||
// for per-user home folders under working/<email>/. Default
|
// operator can set on any auto_own position; the current embedded default
|
||||||
// (nil/false) writes a non-fenced auto-own .zddc — ancestor
|
// tree does NOT set it anywhere — the working/ staging/ incoming/
|
||||||
// admin grants still apply.
|
// reviewing/ <party> homes are auto-owned but UNFENCED, so ancestor grants
|
||||||
|
// (e.g. `project_team: cr` at working/) still cascade in, making them
|
||||||
|
// shared team folders rather than private per-user sandboxes. Default
|
||||||
|
// (nil/false) writes a non-fenced auto-own .zddc.
|
||||||
AutoOwnFenced *bool `yaml:"auto_own_fenced,omitempty" json:"auto_own_fenced,omitempty"`
|
AutoOwnFenced *bool `yaml:"auto_own_fenced,omitempty" json:"auto_own_fenced,omitempty"`
|
||||||
|
|
||||||
// AutoOwnRoles augments AutoOwn with role-level grants: when set,
|
// AutoOwnRoles augments AutoOwn with role-level grants: when set,
|
||||||
|
|
|
||||||
|
|
@ -32,20 +32,21 @@ func WriteAutoOwnZddc(dir, principalEmail string, roles []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteAutoOwnZddcFenced is the same as WriteAutoOwnZddc but additionally
|
// WriteAutoOwnZddcFenced is the same as WriteAutoOwnZddc but additionally
|
||||||
// sets `acl.inherit: false` — fencing ancestor cascade grants. Used at
|
// sets `acl.inherit: false` — fencing ancestor cascade grants so the new
|
||||||
// per-user home folders under working/ where the convention is "private
|
// directory is private to its creator. This is the OPT-IN private-home form:
|
||||||
// by default; owner edits the file to add collaborators."
|
// the current embedded default tree does NOT use it (the working/staging/
|
||||||
|
// incoming/reviewing party homes are unfenced and shared — see AutoOwnFenced
|
||||||
|
// in file.go). An operator opts in by setting auto_own_fenced on a position.
|
||||||
//
|
//
|
||||||
// Without the fence, an ancestor `*: r` (e.g. a project-root grant for
|
// With the fence, an ancestor grant (e.g. `project_team: cr` at working/)
|
||||||
// authenticated users) would let any user read every other user's
|
// does NOT cascade in, so only the creator (and any roles passed below) can
|
||||||
// working subfolder via cascade — defeating the per-user sandbox.
|
// reach the directory.
|
||||||
//
|
//
|
||||||
// roles is the same as for WriteAutoOwnZddc — listed roles get rwcda
|
// roles is the same as for WriteAutoOwnZddc — listed roles get rwcda
|
||||||
// alongside the creator, and like the creator grant they're INSIDE
|
// alongside the creator, and like the creator grant they're INSIDE
|
||||||
// the fence (only resolvable if the role is defined at this level or
|
// the fence (only resolvable if the role is defined at this level or
|
||||||
// in chain.Embedded, since ancestor role definitions are hidden by
|
// in chain.Embedded, since ancestor role definitions are hidden by
|
||||||
// inherit:false). Typically callers using the fenced variant pass nil
|
// inherit:false). Callers using the fenced variant typically pass nil roles.
|
||||||
// roles — per-user homes don't need peer authority.
|
|
||||||
func WriteAutoOwnZddcFenced(dir, principalEmail string, roles []string) error {
|
func WriteAutoOwnZddcFenced(dir, principalEmail string, roles []string) error {
|
||||||
return writeAutoOwn(dir, principalEmail, true, roles)
|
return writeAutoOwn(dir, principalEmail, true, roles)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue