feat(policy): config-edit is a standing permission, not elevation-gated

Editing a .zddc you administer no longer requires toggling admin mode.
Elevation becomes purely additive — it only adds the WORM/destructive
overrides ("things you otherwise couldn't do"), never a prerequisite for
authority you already hold.

Mechanism: a new zddc.IsConfigEditor(chain, email) reports STANDING
config-edit authority — being a subtree admin (admins: cascade) OR holding
the `a` verb — without the elevation gate. InternalDecider.Allow grants
VerbA on that basis ABOVE the WORM clamp: config is not WORM-protected
data, and VerbA only ever authorises .zddc/.zddc.zip/role mutations, never
write/delete of records (those stay clamped + elevation-gated). The full
WORM/ACL bypass (IsActiveAdmin) is unchanged — still admins: + Elevated.

This flows for free to the client: EffectiveVerbsFromChainP loops
ActionAdmin through the decider, so /.profile/access + cap.has(node,'a')
light up the .zddc form editor with no client change, and ServeZddcFile
already gates raw .zddc reads on directory read ACL (config is visible).

A standing subtree admin can thus rewrite their subtree's policy
(admins:/ACL/roles) un-elevated — bounded to their scope (authority
cascades down only, never up), logged, and unable to touch WORM data or
secrets without elevating. That's "admin of X = owns X's policy."

Tests: new TestStandingConfigEdit (decider matrix incl. WORM-transcending
config-edit + data-write still gated); updated the old "un-elevated admin
cannot edit .zddc" invariants (TruthTable, ZddcPut/DeleteMatrix,
NoSilentBypass now scoped to WORM/out-of-scope, profile PathVerbs) to the
new model. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 17:00:54 -05:00
parent 252d3f173e
commit bd219afeb7
6 changed files with 166 additions and 41 deletions

View file

@ -114,16 +114,18 @@ func TestInvariant_UnelevatedAdminCannotBypassWorm(t *testing.T) {
}
}
func TestInvariant_UnelevatedAdminCannotEditZddc(t *testing.T) {
// .zddc edits route through the decider as ActionAdmin. The bypass
// for elevated admins fires only when Principal.Elevated is true.
// Exercised at the HTTP boundary: a PUT to .zddc from an un-elevated
// super-admin must return Forbidden.
func TestInvariant_UnelevatedAdminCanEditZddc(t *testing.T) {
// Config-edit is a STANDING permission: .zddc edits route through the
// decider as ActionAdmin, which IsConfigEditor grants to a subtree
// admin WITHOUT elevation (elevation is reserved for the WORM/
// destructive overrides — see TestInvariant_UnelevatedAdminNoSilentBypass).
// Exercised at the HTTP boundary: a PUT to a .zddc the principal
// administers, from an un-elevated admin, must succeed.
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/working/.zddc"
rec := doReq(cfg, http.MethodPut, target, "admin@example.com", false, []byte("title: mutated\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("un-elevated admin .zddc write succeeded: status=%d body=%s", rec.Code, rec.Body.String())
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
t.Fatalf("un-elevated admin .zddc write blocked: status=%d body=%s", rec.Code, rec.Body.String())
}
}
@ -331,25 +333,30 @@ func TestInvariant_ZddcPutMatrix(t *testing.T) {
who principal
want int
}{
// Root .zddc
// Root .zddc. Config-edit is standing for an admin OF that path: the
// root super-admin edits the root .zddc un-elevated; a subtree admin
// (alice) does NOT administer root, so she's denied either way.
{"root admin elevated → root .zddc", "/.zddc", rootAdminElevated, ok},
{"root admin un-elevated → root .zddc", "/.zddc", rootAdminUnelevated, den},
{"root admin un-elevated → root .zddc", "/.zddc", rootAdminUnelevated, ok},
{"subtree admin elevated → root .zddc", "/.zddc", subtreeAdminElevated, den},
{"subtree admin un-elevated → root .zddc", "/.zddc", subtreeAdminUnelevated, den},
{"non-admin → root .zddc", "/.zddc", nonAdmin, den},
{"anonymous → root .zddc", "/.zddc", anon, den},
// Project .zddc (no on-disk file yet — PUT creates it)
// Project .zddc (no on-disk file yet — PUT creates it). The root admin
// administers all subtrees (cascade), so standing-edits it un-elevated;
// alice's admin scope is below this path, so she's out-of-scope.
{"root admin elevated → project .zddc", "/Project-1/.zddc", rootAdminElevated, http.StatusCreated},
{"root admin un-elevated → project .zddc", "/Project-1/.zddc", rootAdminUnelevated, den},
{"root admin un-elevated → project .zddc", "/Project-1/.zddc", rootAdminUnelevated, http.StatusCreated},
{"subtree admin elevated (out-of-scope) → project .zddc", "/Project-1/.zddc", subtreeAdminElevated, den},
{"non-admin → project .zddc", "/Project-1/.zddc", nonAdmin, den},
// Subtree .zddc (alice administers this subtree)
// Subtree .zddc (alice administers this subtree). Both the root admin
// and alice standing-edit it un-elevated; non-admins/anon denied.
{"root admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminElevated, ok},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, den},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, ok},
{"subtree admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminElevated, ok},
{"subtree admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, den},
{"subtree admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, ok},
{"non-admin → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", nonAdmin, den},
{"anonymous → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", anon, den},
}
@ -392,10 +399,11 @@ func TestInvariant_ZddcDeleteMatrix(t *testing.T) {
who principal
want int
}{
// .zddc DELETE is also ActionAdmin → standing config-edit within scope.
{"root admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminElevated, http.StatusNoContent},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, http.StatusForbidden},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, http.StatusNoContent},
{"subtree admin elevated → own .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminElevated, http.StatusNoContent},
{"subtree admin un-elevated → own .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, http.StatusForbidden},
{"subtree admin un-elevated → own .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, http.StatusNoContent},
{"non-admin → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", nonAdmin, http.StatusForbidden},
}
@ -413,11 +421,13 @@ func TestInvariant_ZddcDeleteMatrix(t *testing.T) {
// ── Invariant 11 — anti-bypass: un-elevated admin gets nothing extra ──────
// TestInvariant_UnelevatedAdminNoSilentBypass is the anti-test for the
// elevation gate. For every (admin-flavour × action) tuple, an
// un-elevated admin must behave exactly like a non-admin: they may
// only do what an explicit ACL grant permits. The fixture's admin and
// alice both have NO baseline ACL grant outside their admin scope, so
// every action below MUST 403 — any pass indicates a bypass leak.
// elevation gate, scoped to what elevation actually guards now: the
// WORM/destructive overrides. Config-edit (ActionAdmin on .zddc) is a
// STANDING permission and is exercised separately (ZddcPutMatrix /
// ZddcDeleteMatrix / UnelevatedAdminCanEditZddc) — it's deliberately NOT
// here. For every (admin-flavour × probe) below, an un-elevated admin must
// behave exactly like a non-admin: WORM records and other principals' homes
// stay off-limits without elevation. Any pass indicates a bypass leak.
func TestInvariant_UnelevatedAdminNoSilentBypass(t *testing.T) {
cfg, _ := invariantsFixture(t)
type op struct {
@ -427,10 +437,6 @@ func TestInvariant_UnelevatedAdminNoSilentBypass(t *testing.T) {
op string
}
probes := []op{
// .zddc writes (ActionAdmin)
{http.MethodPut, "/.zddc", []byte("title: x\n"), ""},
{http.MethodPut, "/Project-1/archive/Acme/working/.zddc", []byte("title: x\n"), ""},
{http.MethodDelete, "/Project-1/archive/Acme/working/.zddc", nil, ""},
// WORM writes (ActionWrite / ActionCreate stripped)
{http.MethodPut, "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md", []byte("# mutate\n"), ""},
{http.MethodPut, "/Project-1/archive/Acme/received/Acme-0042/new.pdf", []byte("%PDF\n"), ""},

View file

@ -553,13 +553,15 @@ acl:
t.Errorf("alice PathCanElevateGrant = %q, want empty (no admin grant on chain)", alice.PathCanElevateGrant)
}
// Un-elevated admin: bypass not active, so explicit verbs are
// whatever ACL granted (here: nothing — admin@ has no permissions
// entry, only an admins: entry). PathCanElevateGrant tells the
// client "elevation would unlock rwcda".
// Un-elevated admin: the WORM/destructive bypass is not active, but
// config-edit is a STANDING permission — being in the admins: cascade
// grants `a` (edit .zddc/.zddc.zip/roles) without elevating. So the
// explicit verbs are exactly "a" even though admin@ has no acl
// permissions entry. PathCanElevateGrant tells the client "elevation
// would unlock the rest (rwcda)".
adminUn := fetch("admin@example.com", false)
if adminUn.PathVerbs != "" {
t.Errorf("un-elevated admin PathVerbs = %q, want empty (no explicit grant)", adminUn.PathVerbs)
if adminUn.PathVerbs != "a" {
t.Errorf("un-elevated admin PathVerbs = %q, want \"a\" (standing config-edit)", adminUn.PathVerbs)
}
if adminUn.PathIsAdmin {
t.Errorf("un-elevated admin PathIsAdmin = true, want false")

View file

@ -235,6 +235,20 @@ func (d *InternalDecider) Allow(_ context.Context, input AllowInput) (bool, erro
return true, nil
}
// Standing config-edit. Authority to mutate configuration (.zddc /
// .zddc.zip / role definitions — the only actions that map to VerbA)
// is a STANDING permission: a subtree admin (admins: cascade) or a
// holder of the `a` verb may edit the config of subtrees they
// administer WITHOUT elevating. This sits ABOVE the WORM clamp because
// config is not WORM-protected data — and it only ever grants VerbA,
// so it can never write/delete/create WORM *records* (those need
// W/C/D, which stay clamped and behind the elevated bypass above).
// Elevation is thus purely additive: it adds the WORM/destructive
// overrides, never gating config-edit you already have authority for.
if verb == zddc.VerbA && zddc.IsConfigEditor(chain, email) {
return true, nil
}
// WORM zone: a directory whose cascade declares `worm:` (see
// internal/zddc/defaults/ — archive/<party>/received and issued carry
// `worm: {}`) is write-locked. Inside it, the effective verbs

View file

@ -54,6 +54,7 @@ func TestAllowActionFromChainP_TruthTable(t *testing.T) {
}
allActions := want{true, true, true, true, true}
noAdmin := want{true, true, true, true, false} // staff has rwcd but no `a`
configOnly := want{adminV: true} // standing config-edit, nothing else
cases := []struct {
name string
@ -76,20 +77,21 @@ func TestAllowActionFromChainP_TruthTable(t *testing.T) {
},
// ─── ELEVATION GATE ─────────────────────────────────────────
// An admin who hasn't elevated MUST be treated as a normal
// user. They don't carry any baseline ACL grant in this
// fixture, so every action is denied.
// An admin who hasn't elevated gets the WORM/destructive bypass
// on NOTHING — but config-edit (the `a` verb) is a STANDING
// permission, so ActionAdmin is allowed while r/w/c/d (no ACL
// grant in this fixture) stay denied. Elevation is additive.
{
name: "root admin NOT elevated → no bypass, no ACL grant → all denied",
name: "root admin NOT elevated → standing config-edit only",
email: "root@example.com",
elevated: false,
want: want{},
want: configOnly,
},
{
name: "subtree admin NOT elevated → no bypass, no ACL grant → all denied",
name: "subtree admin NOT elevated → standing config-edit only",
email: "sub@example.com",
elevated: false,
want: want{},
want: configOnly,
},
// ─── NON-ADMIN PATHS ────────────────────────────────────────
@ -267,9 +269,11 @@ func TestAllowActionFromChainP_BypassWinsOverWorm(t *testing.T) {
})
}
// Negative control: same principal un-elevated must NOT bypass WORM.
// Negative control: same principal un-elevated must NOT bypass WORM for
// DATA ops. Write/Delete (and Create) of records stay clamped — those
// are the destructive overrides elevation exists for.
pUn := zddc.Principal{Email: "root@example.com", Elevated: false}
for _, action := range []string{ActionWrite, ActionDelete, ActionAdmin} {
for _, action := range []string{ActionWrite, ActionDelete} {
t.Run("un-elevated admin in WORM zone — "+action, func(t *testing.T) {
got, _ := AllowActionFromChainP(ctx, d, chain, pUn, "/received/x", action)
if got {
@ -277,4 +281,12 @@ func TestAllowActionFromChainP_BypassWinsOverWorm(t *testing.T) {
}
})
}
// EXCEPTION: ActionAdmin (config-edit) is a STANDING permission and
// transcends the WORM clamp — a subtree admin may fix the .zddc that
// governs a WORM zone without elevating. This grants only VerbA, never
// write/delete of the WORM records themselves (asserted just above).
if got, _ := AllowActionFromChainP(ctx, d, chain, pUn, "/received/.zddc", ActionAdmin); !got {
t.Errorf("un-elevated admin ActionAdmin denied in WORM zone; config-edit should be standing")
}
}

View file

@ -0,0 +1,74 @@
package policy
import (
"context"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// TestStandingConfigEdit pins the elevation-independent config-edit model:
// a subtree admin (admins: cascade) or an `a`-verb holder may edit config
// (ActionAdmin → VerbA) WITHOUT elevating — including above a WORM clamp —
// while WORM *data* writes and the other escape hatches stay behind the
// elevated bypass. See policy.InternalDecider.Allow + zddc.IsConfigEditor.
func TestStandingConfigEdit(t *testing.T) {
d := &InternalDecider{}
dec := func(chain zddc.PolicyChain, p zddc.Principal, action string) bool {
ok, _ := AllowActionFromChainP(context.Background(), d, chain, p, "/proj/probe", action)
return ok
}
alice := func(elev bool) zddc.Principal { return zddc.Principal{Email: "alice@x", Elevated: elev} }
// admins: [alice] — subtree admin via the cascade.
adminChain := zddc.PolicyChain{
Levels: []zddc.ZddcFile{{Admins: []string{"alice@x"}}},
HasAnyFile: true,
}
// acl: alice holds ONLY the `a` verb (config-edit, no rwcd).
aVerbChain := zddc.PolicyChain{
Levels: []zddc.ZddcFile{{ACL: zddc.ACLRules{Permissions: map[string]string{"alice@x": "a"}}}},
HasAnyFile: true,
}
// acl: alice holds rw but NOT a.
rwChain := zddc.PolicyChain{
Levels: []zddc.ZddcFile{{ACL: zddc.ACLRules{Permissions: map[string]string{"alice@x": "rw"}}}},
HasAnyFile: true,
}
// admins: [alice] AND a WORM zone (a non-nil worm list marks the zone).
wormAdminChain := zddc.PolicyChain{
Levels: []zddc.ZddcFile{{Admins: []string{"alice@x"}, Worm: []string{}}},
HasAnyFile: true,
}
cases := []struct {
name string
chain zddc.PolicyChain
p zddc.Principal
action string
want bool
}{
// The headline: a subtree admin edits config without the toggle.
{"subtree admin edits .zddc unelevated", adminChain, alice(false), ActionAdmin, true},
// ...but standing config authority does NOT bleed into data writes.
{"subtree admin data-write still needs elevation", adminChain, alice(false), ActionWrite, false},
{"subtree admin data-write WHEN elevated (bypass)", adminChain, alice(true), ActionWrite, true},
// The `a` verb is standing config-edit on its own, independent of admins:.
{"a-verb holder edits .zddc unelevated", aVerbChain, alice(false), ActionAdmin, true},
{"a-verb holder cannot write data", aVerbChain, alice(false), ActionWrite, false},
// Plain write/read must NOT be able to rewrite policy (no self-escalation).
{"rw-but-not-a cannot edit .zddc", rwChain, alice(false), ActionAdmin, false},
{"rw user can still read", rwChain, alice(false), ActionRead, true},
// A stranger gets nothing.
{"stranger cannot edit .zddc", adminChain, zddc.Principal{Email: "mallory@x"}, ActionAdmin, false},
// Config-edit transcends the WORM clamp (you can fix the policy that
// governs a WORM zone), but WORM data is still protected.
{"config-edit transcends WORM clamp unelevated", wormAdminChain, alice(false), ActionAdmin, true},
{"WORM data write denied to admin unelevated", wormAdminChain, alice(false), ActionWrite, false},
}
for _, tc := range cases {
if got := dec(tc.chain, tc.p, tc.action); got != tc.want {
t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
}
}
}

View file

@ -64,6 +64,23 @@ func IsAdminForChain(chain PolicyChain, email string) bool {
return AdminLevelInChain(chain, email) >= 0
}
// IsConfigEditor reports STANDING authority to edit configuration at this
// chain — writing a .zddc / .zddc.zip / role definition (the mutations that
// map to VerbA). Authority comes from EITHER being a subtree admin (an
// admins: grant anywhere on the chain) OR holding the `a` verb in
// acl.permissions. Unlike IsSubtreeAdmin this is NOT elevation-gated:
// editing config you administer is a standing permission, not a sudo-style
// escape hatch. Elevation (IsActiveAdmin) only ADDS the WORM/destructive
// overrides on top — see policy.InternalDecider.Allow, which consults this
// for VerbA above the WORM clamp (config is not WORM-protected data, and
// VerbA never grants write/delete of records).
func IsConfigEditor(chain PolicyChain, email string) bool {
if email == "" {
return false
}
return IsAdminForChain(chain, email) || AllowedAction(chain, email, VerbA)
}
// HasAnyAdminGrant reports whether email is named as an admin somewhere
// in the cascade — either the root's admins: list (super-admin) or any
// subtree-admin grant via paths:.<dir>.admins. ELEVATION-INDEPENDENT: