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")
|
||
}
|
||
}
|