refactor(policy): drop strict-ancestor rule for .zddc edits
The rule said: an admin granted in /<dir>/.zddc can edit deeper .zddc files but NOT the one that grants their own authority. Intended to prevent self-elevation, peer-addition, and delegator- removal. Three problems: - "Add peers" isn't an attack — it's the common collaboration case. Project creator can't grant a teammate access without bothering a super-admin every time. - "Remove the delegator" doesn't work. Root admin authority lives in the ROOT .zddc and cascades down regardless of what's in /<dir>/.zddc; subtree admins can't touch it. - "Self-elevation" within a subtree is meaningless. They already have rwcda there. Replacement model: admins in /<dir>/.zddc OWN /<dir>/ and everything beneath, including the .zddc itself. They can add collaborators, modify ACLs, even remove themselves. Self-removal is a recoverable footgun — root super-admins always retain authority via the root cascade and can restore. What stays: - The admins: field as a load-bearing key (drives IsActiveAdmin + sudo-style elevation + WORM bypass). - Bootstrap via root .zddc hand-editing. - IsAdminForChain(chain, email, excludeLeaf bool) signature — ModeStrict / NIST AC-6 deployments can still opt into the strict- ancestor walk if they need it. Tests flipped to match the new contract; ProjectCreate flow now gives the creator real control over their project root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b80b11c99f
commit
df19a63853
3 changed files with 32 additions and 22 deletions
|
|
@ -157,26 +157,34 @@ func TestInvariant_ElevatedSubtreeAdminBlockedOutsideScope(t *testing.T) {
|
|||
|
||||
// ── Invariant 4 — .zddc strict-ancestor self-elevation prevention ─────────
|
||||
|
||||
func TestInvariant_SubtreeAdminCannotEditOwnSubtreeZddc(t *testing.T) {
|
||||
// alice's authority comes from /Project-1/working/.zddc. She must
|
||||
// NOT be able to edit that file — strict-ancestor rule prevents
|
||||
// her from adding peers, removing the delegator, or otherwise
|
||||
// self-elevating.
|
||||
// Strict-ancestor was retired — a subtree admin owns their .zddc.
|
||||
// These tests pin the post-change contract: an elevated admin
|
||||
// granted in /<dir>/.zddc CAN edit that file (add collaborators,
|
||||
// adjust ACLs, even — accidentally — remove themselves). Footgun
|
||||
// is recoverable via super-admin restore.
|
||||
|
||||
func TestInvariant_SubtreeAdminCanEditOwnSubtreeZddc(t *testing.T) {
|
||||
cfg, _ := invariantsFixture(t)
|
||||
p := zddc.Principal{Email: "alice@example.com", Elevated: true}
|
||||
dir := filepath.Join(cfg.Root, "Project-1/working")
|
||||
if zddc.CanEditZddc(cfg.Root, dir, p) {
|
||||
t.Fatalf("subtree admin can edit own .zddc — strict-ancestor rule bypassed")
|
||||
chain, err := zddc.EffectivePolicy(cfg.Root, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("EffectivePolicy: %v", err)
|
||||
}
|
||||
if !zddc.IsAdminForChain(chain, p.Email, false) {
|
||||
t.Fatalf("subtree admin lost authority to edit own .zddc — strict-ancestor wasn't supposed to apply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvariant_SubtreeAdminCanEditDeeperZddc(t *testing.T) {
|
||||
// alice's authority over Project-1/working/ should let her create
|
||||
// or edit .zddc files in deeper subtrees (e.g., per-user homes).
|
||||
cfg, _ := invariantsFixture(t)
|
||||
p := zddc.Principal{Email: "alice@example.com", Elevated: true}
|
||||
dir := filepath.Join(cfg.Root, "Project-1/working/eve@example.com")
|
||||
if !zddc.CanEditZddc(cfg.Root, dir, p) {
|
||||
chain, err := zddc.EffectivePolicy(cfg.Root, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("EffectivePolicy: %v", err)
|
||||
}
|
||||
if !zddc.IsAdminForChain(chain, p.Email, false) {
|
||||
t.Fatalf("subtree admin blocked from editing deeper .zddc")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1559,7 +1559,7 @@ body.is-elevated {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-18 15:25:14 · fd4f03a-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-18 15:46:59 · b80b11c-dirty</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
|
|
@ -395,19 +395,21 @@ func AllowFromChainP(ctx context.Context, d Decider, chain zddc.PolicyChain, p z
|
|||
// when (and only when) the caller actually holds elevated admin
|
||||
// authority on this chain.
|
||||
//
|
||||
// Strict-ancestor rule for .zddc edits: action == ActionAdmin signals
|
||||
// a .zddc write, and IsAdminForChain is called with excludeLeaf=true
|
||||
// so the leaf .zddc's own admins entry cannot authorize editing the
|
||||
// file that grants it. Other actions use the full chain walk.
|
||||
//
|
||||
// Use this entry point in write-path handlers (file API, plan-review,
|
||||
// accept-transmittal). Read-path callers that don't need admin
|
||||
// bypass can stay on AllowActionFromChain / AllowFromChain — they
|
||||
// implicitly leave IsActiveAdmin=false.
|
||||
// Strict-ancestor rule: NOT applied by default. A subtree admin whose
|
||||
// admins: entry lives in <dir>/.zddc CAN edit that file — they own
|
||||
// the directory and everything it grants. Footgun: they can also
|
||||
// remove themselves from the admins list (recoverable: a super-admin
|
||||
// always retains authority via the cascade from the root .zddc and
|
||||
// can restore the grant). The prior strict-ancestor mode protected
|
||||
// against peer-addition / delegator-removal but was always partial
|
||||
// (deeper .zddc files were freely editable) and made the common
|
||||
// case — "project creator wants to add a collaborator" — friction-y
|
||||
// enough to be unusable. IsAdminForChain still accepts excludeLeaf
|
||||
// for any caller that wants strict mode; the default path doesn't
|
||||
// fire it.
|
||||
func AllowActionFromChainP(ctx context.Context, d Decider, chain zddc.PolicyChain, p zddc.Principal, path, action string) (bool, error) {
|
||||
excludeLeaf := action == ActionAdmin
|
||||
isAdmin := p.Elevated && p.Email != "" &&
|
||||
zddc.IsAdminForChain(chain, p.Email, excludeLeaf)
|
||||
zddc.IsAdminForChain(chain, p.Email, false)
|
||||
in := AllowInput{Path: path, Action: action, PolicyChain: chainToSerializable(chain)}
|
||||
in.User.Email = p.Email
|
||||
in.User.IsActiveAdmin = isAdmin
|
||||
|
|
|
|||
Loading…
Reference in a new issue