package handler // Layer 2 — the SHIPPED DEFAULT POLICY contract. // // This is the executable truth table for the embedded defaults // (internal/zddc/defaults/): 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) } } }