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>
292 lines
9.4 KiB
Go
292 lines
9.4 KiB
Go
package policy
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
|
||
)
|
||
|
||
// TestAllowActionFromChainP_TruthTable pins the principal-aware decider
|
||
// across the full {elevated × admin-at-level-N × action} cross-product.
|
||
// This is the single bypass site that consolidates every former
|
||
// scattered IsAdmin/IsSubtreeAdmin/CanEditZddc check in handler code,
|
||
// so its semantics must be locked in by an exhaustive table.
|
||
//
|
||
// Invariants pinned:
|
||
//
|
||
// 1. Admin bypass requires BOTH (Email in admins:) AND Elevated.
|
||
// - In admins + elevated → bypass (any action returns true)
|
||
// - In admins + un-elevated → no bypass (falls through to ACL)
|
||
// - Not in admins + elevated → no bypass
|
||
// - Empty email + elevated → no bypass (gate() rejects empty)
|
||
//
|
||
// 2. Bypass is action-agnostic: ActionRead, ActionWrite, ActionCreate,
|
||
// ActionDelete, ActionAdmin all behave the same way under bypass.
|
||
//
|
||
// 3. Admin authority at ANY level on the chain confers bypass
|
||
// (root admin gets bypass even on deep paths; subtree admin
|
||
// declared at level N gets bypass for level ≥ N).
|
||
//
|
||
// 4. With no bypass, the cascade ACL governs:
|
||
// - rwcd grant → ActionRead/Write/Create/Delete succeed, ActionAdmin denied
|
||
// - no grant + has_any_file → all actions denied
|
||
// - empty chain → all actions allowed (public default)
|
||
func TestAllowActionFromChainP_TruthTable(t *testing.T) {
|
||
// Chain shape used throughout: root admins:[root@example.com] +
|
||
// level 1 admins:[sub@example.com] + level 1 ACL allowing
|
||
// staff@example.com rwcd.
|
||
chain := zddc.PolicyChain{
|
||
HasAnyFile: true,
|
||
Levels: []zddc.ZddcFile{
|
||
{Admins: []string{"root@example.com"}},
|
||
{
|
||
Admins: []string{"sub@example.com"},
|
||
ACL: zddc.ACLRules{Permissions: map[string]string{
|
||
"staff@example.com": "rwcd",
|
||
}},
|
||
},
|
||
},
|
||
}
|
||
|
||
type want struct {
|
||
read, write, create, deleteV, adminV bool
|
||
}
|
||
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
|
||
email string
|
||
elevated bool
|
||
want want
|
||
}{
|
||
// ─── BYPASS PATH ────────────────────────────────────────────
|
||
{
|
||
name: "root admin elevated → bypass on every action",
|
||
email: "root@example.com",
|
||
elevated: true,
|
||
want: allActions,
|
||
},
|
||
{
|
||
name: "subtree admin elevated → bypass on every action",
|
||
email: "sub@example.com",
|
||
elevated: true,
|
||
want: allActions,
|
||
},
|
||
|
||
// ─── ELEVATION GATE ─────────────────────────────────────────
|
||
// 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 → standing config-edit only",
|
||
email: "root@example.com",
|
||
elevated: false,
|
||
want: configOnly,
|
||
},
|
||
{
|
||
name: "subtree admin NOT elevated → standing config-edit only",
|
||
email: "sub@example.com",
|
||
elevated: false,
|
||
want: configOnly,
|
||
},
|
||
|
||
// ─── NON-ADMIN PATHS ────────────────────────────────────────
|
||
{
|
||
name: "non-admin with rwcd grant → ACL governs, admin denied",
|
||
email: "staff@example.com",
|
||
elevated: false,
|
||
want: noAdmin,
|
||
},
|
||
{
|
||
name: "non-admin elevated → elevation alone confers nothing",
|
||
email: "staff@example.com",
|
||
elevated: true,
|
||
want: noAdmin,
|
||
},
|
||
{
|
||
name: "stranger denied across the board",
|
||
email: "rando@example.com",
|
||
elevated: false,
|
||
want: want{},
|
||
},
|
||
{
|
||
name: "stranger elevated still denied",
|
||
email: "rando@example.com",
|
||
elevated: true,
|
||
want: want{},
|
||
},
|
||
|
||
// ─── ANONYMOUS / DEGENERATE ─────────────────────────────────
|
||
{
|
||
name: "empty email + elevated → gate rejects, no bypass",
|
||
email: "",
|
||
elevated: true,
|
||
want: want{},
|
||
},
|
||
{
|
||
name: "empty email + not elevated → denied",
|
||
email: "",
|
||
elevated: false,
|
||
want: want{},
|
||
},
|
||
}
|
||
|
||
d := &InternalDecider{}
|
||
ctx := context.Background()
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
p := zddc.Principal{Email: tc.email, Elevated: tc.elevated}
|
||
check := func(action string, want bool) {
|
||
t.Helper()
|
||
got, err := AllowActionFromChainP(ctx, d, chain, p, "/sub/file", action)
|
||
if err != nil {
|
||
t.Fatalf("%s: unexpected error: %v", action, err)
|
||
}
|
||
if got != want {
|
||
t.Errorf("%s: got %v, want %v", action, got, want)
|
||
}
|
||
}
|
||
check(ActionRead, tc.want.read)
|
||
check(ActionWrite, tc.want.write)
|
||
check(ActionCreate, tc.want.create)
|
||
check(ActionDelete, tc.want.deleteV)
|
||
check(ActionAdmin, tc.want.adminV)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAllowActionFromChainP_AdminScopeDepth: admin authority at the
|
||
// root level cascades to every depth; subtree admin authority declared
|
||
// at level N applies only when level N is on the queried chain. The
|
||
// decider doesn't synthesise admin authority — it derives it from
|
||
// IsAdminForChain, which walks the chain it was given.
|
||
func TestAllowActionFromChainP_AdminScopeDepth(t *testing.T) {
|
||
rootOnly := zddc.PolicyChain{
|
||
HasAnyFile: true,
|
||
Levels: []zddc.ZddcFile{
|
||
{Admins: []string{"root@example.com"}},
|
||
},
|
||
}
|
||
rootPlusProject := zddc.PolicyChain{
|
||
HasAnyFile: true,
|
||
Levels: []zddc.ZddcFile{
|
||
{Admins: []string{"root@example.com"}},
|
||
{Admins: []string{"alice@example.com"}},
|
||
},
|
||
}
|
||
siblingChain := zddc.PolicyChain{
|
||
HasAnyFile: true,
|
||
Levels: []zddc.ZddcFile{
|
||
{Admins: []string{"root@example.com"}},
|
||
// Sibling project — alice is NOT in this chain's admins.
|
||
{Admins: []string{"bob@example.com"}},
|
||
},
|
||
}
|
||
|
||
d := &InternalDecider{}
|
||
ctx := context.Background()
|
||
|
||
cases := []struct {
|
||
name string
|
||
chain zddc.PolicyChain
|
||
email string
|
||
path string
|
||
wantPut bool
|
||
}{
|
||
{
|
||
name: "root admin reaches a root-only path",
|
||
chain: rootOnly,
|
||
email: "root@example.com",
|
||
path: "/file",
|
||
wantPut: true,
|
||
},
|
||
{
|
||
name: "root admin reaches a deep path",
|
||
chain: rootPlusProject,
|
||
email: "root@example.com",
|
||
path: "/Project-A/file",
|
||
wantPut: true,
|
||
},
|
||
{
|
||
name: "subtree admin reaches their own subtree",
|
||
chain: rootPlusProject,
|
||
email: "alice@example.com",
|
||
path: "/Project-A/file",
|
||
wantPut: true,
|
||
},
|
||
{
|
||
name: "subtree admin does NOT reach a sibling subtree",
|
||
chain: siblingChain,
|
||
email: "alice@example.com",
|
||
path: "/Project-B/file",
|
||
wantPut: false,
|
||
},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
p := zddc.Principal{Email: tc.email, Elevated: true}
|
||
got, _ := AllowActionFromChainP(ctx, d, tc.chain, p, tc.path, ActionWrite)
|
||
if got != tc.wantPut {
|
||
t.Errorf("AllowActionFromChainP write: got %v, want %v", got, tc.wantPut)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAllowActionFromChainP_BypassWinsOverWorm: an elevated admin's
|
||
// bypass fires before WORM evaluation, so a mis-filed document under
|
||
// received/ or issued/ can still be corrected. This is the explicit
|
||
// human escape hatch documented in the policy package comment.
|
||
func TestAllowActionFromChainP_BypassWinsOverWorm(t *testing.T) {
|
||
trueP := true
|
||
chain := zddc.PolicyChain{
|
||
HasAnyFile: true,
|
||
Levels: []zddc.ZddcFile{
|
||
{Admins: []string{"root@example.com"}},
|
||
{
|
||
// WORM zone (received/issued style). Without admin bypass,
|
||
// every write would be stripped.
|
||
Worm: []string{"_doc_controller"},
|
||
ACL: zddc.ACLRules{Inherit: &trueP},
|
||
},
|
||
},
|
||
}
|
||
d := &InternalDecider{}
|
||
ctx := context.Background()
|
||
|
||
p := zddc.Principal{Email: "root@example.com", Elevated: true}
|
||
for _, action := range []string{ActionRead, ActionWrite, ActionCreate, ActionDelete, ActionAdmin} {
|
||
t.Run("elevated admin in WORM zone — "+action, func(t *testing.T) {
|
||
got, _ := AllowActionFromChainP(ctx, d, chain, p, "/received/x", action)
|
||
if !got {
|
||
t.Errorf("elevated admin %s denied inside WORM zone", action)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 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} {
|
||
t.Run("un-elevated admin in WORM zone — "+action, func(t *testing.T) {
|
||
got, _ := AllowActionFromChainP(ctx, d, chain, pUn, "/received/x", action)
|
||
if got {
|
||
t.Errorf("un-elevated admin %s allowed inside WORM zone (bypass leaked)", action)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
}
|