ZDDC/zddc/internal/handler/defaults_matrix_test.go
ZDDC 1e0e403f1e feat(zddc): retire defaults.zddc.yaml; .zddc.zip is the policy carrier (phase 6)
Completes the migration. The embedded per-depth tree (internal/zddc/defaults/)
is now the sole source of the shipped baseline; defaults.zddc.yaml is deleted.

  - EmbeddedDefaults() assembles the tree (no yaml). show-defaults now emits a
    .zddc.zip (per-depth, "*" wildcard members) via EmbeddedDefaultsZip() —
    operators redirect it to <ROOT>/.zddc.zip (or any directory) and edit/add/
    delete individual members.
  - Dropped EmbeddedDefaultsBytes; reworked the dumpable test to validate the
    emitted zip; removed the now-redundant tree-vs-yaml oracle (the Layer-2
    matrix is the ongoing behavioral guarantee, and it stays green).
  - Swept stale "defaults.zddc.yaml" comment references to the embedded tree.
  - GRAMMAR.md §1/§6 updated: .zddc.zip is a policy bundle mountable at ANY
    directory (subtree mount; inherit:false + acl.inherit:false = island); the
    shipped baseline is the embedded bundle at the root.

Net of the 6-phase migration: policy is per-depth .zddc files in a .zddc.zip
that an operator can drop at any level to override the cascade; the engine
(Assemble + the unchanged walker) enforces it. Full Go suite + matrix green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:35:21 -05:00

152 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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