test(policy): Layer-2 default-policy matrix — role × path × verb truth table

The executable contract for the shipped defaults (internal/zddc/defaults.zddc.yaml):
~38 cells asserting who-can-do-what across the canonical project folders, routed
through the real decider (InternalDecider: cascade + WORM mask + active-admin
bypass) evaluated at the target's logical parent — the same decision the server
makes. Locks the document-control model so a change to the defaults OR the
engine that resolves them can't silently shift access. Storage-agnostic: if the
defaults later move into a project-root .zddc.zip of per-depth .zddc files, the
test is unchanged (it asserts effective policy, not where the bytes live).

Covers: no-create-at-project-root; DC/team/observer per-peer grants (working/
staging/reviewing/incoming/ssr); team rwc on mdl/rsk; archive WORM (DC
create-once, no write/delete; others read); elevated-admin bypass vs un-elevated
no-bypass; anonymous denied. Complements Layer 1 (engine-follows-policy):
policy.TestInternalDecider_CascadeScenarios + zddc/{acl,roles,worm}_test +
policy/parity_test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 10:01:29 -05:00
parent 3ac53fe894
commit bae8e1f79b

View file

@ -0,0 +1,152 @@
package handler
// Layer 2 — the SHIPPED DEFAULT POLICY contract.
//
// This is the executable truth table for the embedded defaults
// (internal/zddc/defaults.zddc.yaml): role × canonical-path × verb → allow/deny.
// It pins the document-control access model so a change to the defaults — OR to
// the engine that resolves them — can't silently alter who-can-do-what. (When
// the defaults later move into a project-root .zddc.zip of per-depth .zddc
// files, this test is unchanged: it asserts EFFECTIVE policy, not where the
// bytes live.)
//
// Two layers, deliberately separate:
// - Layer 1 (engine follows whatever policy says): policy.TestInternalDecider_
// CascadeScenarios + internal/zddc/{acl,roles,worm}_test.go (synthetic
// policies) + internal/policy/parity_test.go (InternalDecider ↔ OPA).
// - Layer 2 (the shipped defaults are correct): THIS file.
//
// Decisions go through the same decider the server uses (InternalDecider, which
// applies the cascade + WORM mask + active-admin bypass), evaluated at the
// target's logical parent — mirroring authorizeAction. The HTTP plumbing that
// chooses that path is covered separately by the auth_invariants tests.
import (
"context"
"path/filepath"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/policy"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// defaultsMatrixFixture is a minimal operator deployment: it only populates the
// three standard roles (which the embedded defaults ship empty) plus one admin,
// and registers party Acme (party_source: ssr gates the peers). Every grant in
// the matrix below therefore comes from the embedded defaults, not the fixture.
func defaultsMatrixFixture(t *testing.T) config.Config {
t.Helper()
root := t.TempDir()
mustWriteHelper(t, filepath.Join(root, ".zddc"),
"admins:\n - admin@x\n"+
"roles:\n"+
" document_controller:\n members: [dc@x]\n"+
" project_team:\n members: [team@x]\n"+
" observer:\n members: [obs@x]\n")
mustWriteHelper(t, filepath.Join(root, "Proj/ssr/Acme.yaml"), "kind: SSR\n")
zddc.InvalidateCache(root)
return config.Config{Root: root, EmailHeader: "X-Auth-Request-Email", MaxWriteBytes: 64 * 1024}
}
// canDo reports whether <email> (elevated?) may perform <action> on content in
// <dir> — the chain is resolved at <dir> (the logical parent of the child being
// acted on) and routed through the internal decider, exactly as the server's
// authorizeAction does for a create/write/delete/read.
func canDo(t *testing.T, cfg config.Config, email string, elevated bool, dir, action string) bool {
t.Helper()
p := zddc.Principal{Email: email, Elevated: elevated}
chain, err := zddc.EffectivePolicy(cfg.Root, filepath.Join(cfg.Root, filepath.FromSlash(dir)))
if err != nil {
t.Fatalf("EffectivePolicy(%s): %v", dir, err)
}
allowed, _ := policy.AllowActionFromChainP(
context.Background(), &policy.InternalDecider{}, chain, p, "/"+dir+"/probe", action)
return allowed
}
func TestDefaultPolicyMatrix(t *testing.T) {
cfg := defaultsMatrixFixture(t)
const (
R = policy.ActionRead
W = policy.ActionWrite
C = policy.ActionCreate
D = policy.ActionDelete
)
cases := []struct {
note string
who string
elev bool
dir string
action string
want bool
}{
// ── Project root: standard peers only; no create for anyone ──────────
{"team: read project root", "team@x", false, "Proj", R, true},
{"observer: read project root", "obs@x", false, "Proj", R, true},
{"team: NO create at project root", "team@x", false, "Proj", C, false},
{"DC: NO create at project root", "dc@x", false, "Proj", C, false},
// ── working/<party>: DC rwcda, team cr, observer r ───────────────────
{"DC: create in working", "dc@x", false, "Proj/working/Acme", C, true},
{"team: create in working", "team@x", false, "Proj/working/Acme", C, true},
{"team: read working", "team@x", false, "Proj/working/Acme", R, true},
{"observer: read working", "obs@x", false, "Proj/working/Acme", R, true},
{"observer: NO create in working", "obs@x", false, "Proj/working/Acme", C, false},
// nested under working — the path the authorizeAction bug denied
{"DC: create nested in working", "dc@x", false, "Proj/working/Acme/sub", C, true},
{"team: create nested in working", "team@x", false, "Proj/working/Acme/sub", C, true},
// ── staging / reviewing: team cr ─────────────────────────────────────
{"team: create in staging", "team@x", false, "Proj/staging/Acme", C, true},
{"team: create in reviewing", "team@x", false, "Proj/reviewing/Acme", C, true},
// ── incoming: DC rwcd, team read-only ────────────────────────────────
{"DC: create in incoming", "dc@x", false, "Proj/incoming/Acme", C, true},
{"team: NO create in incoming", "team@x", false, "Proj/incoming/Acme", C, false},
{"team: read incoming", "team@x", false, "Proj/incoming/Acme", R, true},
// ── ssr (party registry): DC rwc, team read-only ─────────────────────
{"DC: register party (create in ssr)", "dc@x", false, "Proj/ssr", C, true},
{"team: NO create in ssr", "team@x", false, "Proj/ssr", C, false},
{"team: read ssr", "team@x", false, "Proj/ssr", R, true},
// ── mdl / rsk registers: DC rwcd, team rwc (no delete), observer r ───
{"DC: create mdl row", "dc@x", false, "Proj/mdl/Acme", C, true},
{"DC: delete mdl row", "dc@x", false, "Proj/mdl/Acme", D, true},
{"team: create mdl row", "team@x", false, "Proj/mdl/Acme", C, true},
{"team: edit mdl row", "team@x", false, "Proj/mdl/Acme", W, true},
{"team: NO delete mdl row", "team@x", false, "Proj/mdl/Acme", D, false},
{"observer: NO create mdl row", "obs@x", false, "Proj/mdl/Acme", C, false},
{"team: create rsk row", "team@x", false, "Proj/rsk/Acme", C, true},
{"team: edit rsk row", "team@x", false, "Proj/rsk/Acme", W, true},
{"team: NO delete rsk row", "team@x", false, "Proj/rsk/Acme", D, false},
// ── archive WORM: DC create-once, no write/delete; others read ───────
{"DC: worm-create in received", "dc@x", false, "Proj/archive/Acme/received", C, true},
{"DC: NO write in WORM received", "dc@x", false, "Proj/archive/Acme/received", W, false},
{"DC: NO delete in WORM issued", "dc@x", false, "Proj/archive/Acme/issued", D, false},
{"team: NO create in archive", "team@x", false, "Proj/archive/Acme/issued", C, false},
{"team: read archive", "team@x", false, "Proj/archive/Acme/issued", R, true},
// ── Elevated admin: full bypass (the human escape hatch) ─────────────
{"elevated admin: bypass WORM write", "admin@x", true, "Proj/archive/Acme/issued", W, true},
{"elevated admin: create in working", "admin@x", true, "Proj/working/Acme", C, true},
// ── Un-elevated admin: NO bypass; not in any role → no grant ─────────
{"un-elevated admin: NO WORM bypass", "admin@x", false, "Proj/archive/Acme/issued", W, false},
{"un-elevated admin: NO create in working", "admin@x", false, "Proj/working/Acme", C, false},
// ── Anonymous: nothing (a .zddc exists → no public default) ──────────
{"anon: NO read working", "", false, "Proj/working/Acme", R, false},
{"anon: NO create working", "", false, "Proj/working/Acme", C, false},
}
for _, tc := range cases {
got := canDo(t, cfg, tc.who, tc.elev, tc.dir, tc.action)
if got != tc.want {
t.Errorf("%s — canDo(%q, elevated=%v, %s, %q) = %v, want %v",
tc.note, tc.who, tc.elev, tc.dir, tc.action, got, tc.want)
}
}
}