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>
74 lines
3.2 KiB
Go
74 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|