From bae8e1f79bb1eb3e0a6677c605a1da62577265b9 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Fri, 5 Jun 2026 10:01:29 -0500 Subject: [PATCH] =?UTF-8?q?test(policy):=20Layer-2=20default-policy=20matr?= =?UTF-8?q?ix=20=E2=80=94=20role=20=C3=97=20path=20=C3=97=20verb=20truth?= =?UTF-8?q?=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- zddc/internal/handler/defaults_matrix_test.go | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 zddc/internal/handler/defaults_matrix_test.go diff --git a/zddc/internal/handler/defaults_matrix_test.go b/zddc/internal/handler/defaults_matrix_test.go new file mode 100644 index 0000000..6cd56ee --- /dev/null +++ b/zddc/internal/handler/defaults_matrix_test.go @@ -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 (elevated?) may perform on content in +// — the chain is resolved at (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/: 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) + } + } +}