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) } } } // TestStandingConfigEdit_WormDemotionIsTwoStep documents the (intended) // composition that a single-action view of the decider hides: a config-editor // who administers a WORM zone cannot write a WORM record directly, but CAN // demote the zone by editing its own .zddc, after which an ordinary write is // no longer clamped. WORM is thus tamper-evident to its policy owner, not // tamper-proof. Pinned so the behavior is an explicit, tested decision — if a // deployment ever needs WORM markers immutable except under elevation, this is // the test that must change alongside gating worm: relaxation behind // IsActiveAdmin in policy.InternalDecider.Allow. func TestStandingConfigEdit_WormDemotionIsTwoStep(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 := zddc.Principal{Email: "alice@x", Elevated: false} // config-editor, NOT elevated // Before — alice administers a WORM zone (admins: + a non-nil worm list). worm := zddc.PolicyChain{ Levels: []zddc.ZddcFile{{Admins: []string{"alice@x"}, Worm: []string{}}}, HasAnyFile: true, } // The boundary holds: a direct WORM record write is denied unelevated... if dec(worm, alice, ActionWrite) { t.Error("unelevated config-editor must NOT directly write a WORM record") } // ...but she CAN edit the zone's policy (VerbA) — the lever for demotion. if !dec(worm, alice, ActionAdmin) { t.Error("config-editor should be able to edit the WORM zone's .zddc unelevated") } // After — alice has rewritten that .zddc: inherit:false dropped the // embedded worm: and her acl now grants rwcd (the post-edit cascade the // file API persists). The subtree is no longer WORM, so her write lands — // still unelevated. This is step two of the intended demotion. demoted := zddc.PolicyChain{ Levels: []zddc.ZddcFile{{ACL: zddc.ACLRules{Permissions: map[string]string{"alice@x": "rwcd"}}}}, HasAnyFile: true, } if !dec(demoted, alice, ActionWrite) { t.Error("after the config-editor demotes the zone, the ordinary write should be allowed") } }