ZDDC/zddc/internal/handler/auth_invariants_test.go
ZDDC 59b5550872 refactor: nest lifecycle slots per-party + add virtual top-level aggregators
May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:

  ssr/mdl/rsk           tables rollups across parties with a
                        synthesised $party source-party column
  working/staging/      browse folder-nav listings of parties with
  reviewing             non-empty content in the slot; per-party
                        URLs 302-redirect to archive/<party>/<slot>/

Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.

Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.

document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:57:45 -05:00

471 lines
20 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
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// auth_invariants_test.go — behavioral lock-in for the admin/elevation/
// WORM invariants. These tests must pass against the CURRENT code before
// the consolidation refactor (single bypass site in InternalDecider) so
// the refactor can be validated against a green baseline.
//
// Each test covers one invariant called out in the security audit. The
// names are deliberately verbose — when one fails, the failure message
// alone tells you which property got broken.
// invariantsFixture sets up a synthetic ZDDC root with:
//
// - admin@example.com — root super-admin
// - alice@example.com — subtree admin of Project-1/archive/Acme/working
// (via per-dir .zddc admins:) — used to test
// subtree scope
// - bob@example.com — document_controller role member (gets WORM cr
// on received/ + issued/ via cascade defaults)
// - eve@example.com — non-admin, project_team only (read-only across
// the project per defaults)
//
// Plus one file each in working/, issued/, received/ so we can exercise
// reads + writes across the cascade.
func invariantsFixture(t *testing.T) (config.Config, string) {
t.Helper()
root := t.TempDir()
mustWriteHelper(t, filepath.Join(root, ".zddc"),
"admins:\n - admin@example.com\n"+
"roles:\n"+
" document_controller:\n members: [bob@example.com]\n"+
" project_team:\n members: [\"*@example.com\"]\n")
for _, d := range []string{
"Project-1/archive/Acme/working/eve@example.com",
"Project-1/archive/Acme/received/Acme-0042",
"Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test",
} {
if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", d, err)
}
}
// Subtree-admin grant: alice administers Project-1/archive/Acme/working/.
mustWriteHelper(t,
filepath.Join(root, "Project-1/archive/Acme/working/.zddc"),
"admins:\n - alice@example.com\n")
// Files to act on.
mustWriteHelper(t,
filepath.Join(root, "Project-1/archive/Acme/working/eve@example.com/draft.md"),
"# eve's draft\n")
mustWriteHelper(t,
filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042/Acme-0042_A (RFI) - Test.pdf"),
"%PDF-A\n")
mustWriteHelper(t,
filepath.Join(root, "Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md"),
"# issued draft\n")
zddc.InvalidateCache(root)
return config.Config{
Root: root,
EmailHeader: "X-Auth-Request-Email",
MaxWriteBytes: 64 * 1024,
}, root
}
// do executes a request with the given email / elevation flag. URL-encoding
// is computed from the path so spaces and parens (real ZDDC filenames)
// round-trip cleanly.
func doReq(cfg config.Config, method, urlPath, email string, elevated bool, body []byte, op string) *httptest.ResponseRecorder {
u := &url.URL{Path: urlPath}
req := httptest.NewRequest(method, u.RequestURI(), bytes.NewReader(body))
if op != "" {
req.Header.Set(headerOp, op)
}
ctx := context.WithValue(req.Context(), EmailKey, email)
ctx = context.WithValue(ctx, ElevatedKey, elevated)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
ServeFileAPI(cfg, rec, req)
return rec
}
// ── Invariant 1 — Un-elevated admin has no admin authority ────────────────
func TestInvariant_UnelevatedAdminCannotBypassWorm(t *testing.T) {
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md"
rec := doReq(cfg, http.MethodPut, target, "admin@example.com", false, []byte("# mutated\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("un-elevated admin write succeeded: status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestInvariant_UnelevatedAdminCannotEditZddc(t *testing.T) {
// .zddc edits route through the decider as ActionAdmin. The bypass
// for elevated admins fires only when Principal.Elevated is true.
// Exercised at the HTTP boundary: a PUT to .zddc from an un-elevated
// super-admin must return Forbidden.
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/working/.zddc"
rec := doReq(cfg, http.MethodPut, target, "admin@example.com", false, []byte("title: mutated\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("un-elevated admin .zddc write succeeded: status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestInvariant_ElevatedAdminCanEditZddc(t *testing.T) {
// Positive control: a super-admin who has elevated CAN write any
// .zddc. The decider's IsActiveAdmin short-circuit fires in
// AllowActionFromChainP and the file API write proceeds.
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/working/.zddc"
rec := doReq(cfg, http.MethodPut, target, "admin@example.com", true, []byte("title: elevated edit\n"), "")
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
t.Fatalf("elevated admin .zddc write blocked: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 2 — Elevated admin can do everything (positive control) ─────
func TestInvariant_ElevatedAdminBypassesWorm(t *testing.T) {
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md"
rec := doReq(cfg, http.MethodPut, target, "admin@example.com", true, []byte("# fix-mis-filed\n"), "")
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
t.Fatalf("elevated admin write blocked: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 3 — Subtree admin scope ──────────────────────────────────────
func TestInvariant_ElevatedSubtreeAdminWritesInScope(t *testing.T) {
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/working/eve@example.com/draft.md"
rec := doReq(cfg, http.MethodPut, target, "alice@example.com", true, []byte("# alice override\n"), "")
// alice is subtree admin of Project-1/archive/Acme/working/ — should override eve's
// fenced auto-own and write through.
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
t.Fatalf("elevated subtree admin write in scope blocked: status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestInvariant_ElevatedSubtreeAdminBlockedOutsideScope(t *testing.T) {
cfg, _ := invariantsFixture(t)
// alice is subtree admin of /Project-1/archive/Acme/working/, NOT of /Project-1/archive/.
target := "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md"
rec := doReq(cfg, http.MethodPut, target, "alice@example.com", true, []byte("# out-of-scope\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("subtree admin escaped scope: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 4 — .zddc strict-ancestor self-elevation prevention ─────────
// Strict-ancestor was retired — a subtree admin owns their .zddc.
// These tests pin the post-change contract: an elevated admin
// granted in /<dir>/.zddc CAN edit that file (add collaborators,
// adjust ACLs, even — accidentally — remove themselves). Footgun
// is recoverable via super-admin restore.
func TestInvariant_SubtreeAdminCanEditOwnSubtreeZddc(t *testing.T) {
cfg, _ := invariantsFixture(t)
p := zddc.Principal{Email: "alice@example.com", Elevated: true}
dir := filepath.Join(cfg.Root, "Project-1/archive/Acme/working")
chain, err := zddc.EffectivePolicy(cfg.Root, dir)
if err != nil {
t.Fatalf("EffectivePolicy: %v", err)
}
if !zddc.IsAdminForChain(chain, p.Email) {
t.Fatalf("subtree admin lost authority to edit own .zddc — strict-ancestor wasn't supposed to apply")
}
}
func TestInvariant_SubtreeAdminCanEditDeeperZddc(t *testing.T) {
cfg, _ := invariantsFixture(t)
p := zddc.Principal{Email: "alice@example.com", Elevated: true}
dir := filepath.Join(cfg.Root, "Project-1/archive/Acme/working/eve@example.com")
chain, err := zddc.EffectivePolicy(cfg.Root, dir)
if err != nil {
t.Fatalf("EffectivePolicy: %v", err)
}
if !zddc.IsAdminForChain(chain, p.Email) {
t.Fatalf("subtree admin blocked from editing deeper .zddc")
}
}
// ── Invariant 5 — Empty email never matches ────────────────────────────────
func TestInvariant_EmptyEmailHasNoAuthority(t *testing.T) {
cfg, _ := invariantsFixture(t)
target := "/Project-1/archive/Acme/working/eve@example.com/draft.md"
rec := doReq(cfg, http.MethodPut, target, "", true, []byte("# anon\n"), "")
if rec.Code != http.StatusForbidden && rec.Code != http.StatusUnauthorized {
t.Fatalf("empty-email write succeeded: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 6 — WORM cr survives for document_controller (no admin) ─────
func TestInvariant_DocControllerCanCreateInWormZone(t *testing.T) {
cfg, _ := invariantsFixture(t)
// bob is a document_controller (per role membership) but NOT an admin.
// He must be able to CREATE new files in received/<tracking>/ even
// without elevation — the WORM cr grant carries.
target := "/Project-1/archive/Acme/received/Acme-0042/Acme-0042_B (RFI) - Followup.pdf"
rec := doReq(cfg, http.MethodPut, target, "bob@example.com", false, []byte("%PDF-B\n"), "")
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
t.Fatalf("doc_controller blocked from WORM create: status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestInvariant_DocControllerCannotOverwriteInWormZone(t *testing.T) {
cfg, _ := invariantsFixture(t)
// bob can CREATE in WORM but cannot OVERWRITE — the worm strip
// removes w/d for everyone, even WORM-listed principals.
target := "/Project-1/archive/Acme/received/Acme-0042/Acme-0042_A (RFI) - Test.pdf"
rec := doReq(cfg, http.MethodPut, target, "bob@example.com", false, []byte("%PDF-mutated\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("doc_controller bypassed WORM overwrite-strip: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 7 — project_team has read but no write ──────────────────────
func TestInvariant_ProjectTeamCanReadCannotWrite(t *testing.T) {
cfg, _ := invariantsFixture(t)
// eve is project_team (r at project level) and the file lives under
// her own working/ home — but she is NOT in any admin list and not
// elevated, so writes must be ACL-gated.
//
// In her own home, eve has auto-own rwcda via the working/<email>/
// auto-own pattern; the cascade gives her create+write there. So
// the right test is a write OUTSIDE her home — into a peer's area
// or into archive.
target := "/Project-1/archive/Acme/received/Acme-0042/Acme-0042_A (RFI) - Test.pdf"
rec := doReq(cfg, http.MethodPut, target, "eve@example.com", false, []byte("# eve overwrite\n"), "")
if rec.Code != http.StatusForbidden {
t.Fatalf("project_team escaped WORM strip: status=%d body=%s", rec.Code, rec.Body.String())
}
}
// ── Invariant 8 — Forward-auth endpoint requires admin membership ─────────
func TestInvariant_ForwardAuthEndpointGatesOnAdminsList(t *testing.T) {
cfg, _ := invariantsFixture(t)
for _, tc := range []struct {
email string
want int
why string
}{
{"admin@example.com", http.StatusOK, "root admin"},
{"alice@example.com", http.StatusForbidden, "subtree admin only — /.auth/admin gates on ROOT admins:, not subtree"},
{"eve@example.com", http.StatusForbidden, "non-admin"},
{"", http.StatusForbidden, "anonymous"},
} {
req := httptest.NewRequest(http.MethodGet, "/.auth/admin", nil)
ctx := context.WithValue(req.Context(), EmailKey, tc.email)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
ServeAuthAdmin(cfg, rec, req)
if rec.Code != tc.want {
t.Errorf("/.auth/admin for %q (%s): got %d, want %d",
tc.email, tc.why, rec.Code, tc.want)
}
}
}
// ── Invariant 10 — .zddc write matrix at root / project / subtree ─────────
// TestInvariant_ZddcPutMatrix exercises every (principal × elevation ×
// target) combination for PUT to a .zddc file. The decider's
// IsActiveAdmin short-circuit is the single bypass; this matrix locks
// down that it only fires for an Elevated principal who is named in
// the admins: list of some level on the target's chain.
//
// Targets:
// - /.zddc — root file (root admins: govern)
// - /Project-1/.zddc — project file (no on-disk .zddc;
// write must materialise it; root
// admins still govern via cascade)
// - /Project-1/archive/Acme/working/.zddc — subtree file; alice administers
// this subtree via its own admins:
// list (so alice's write doesn't
// require root-admin authority).
//
// Expected status: 200 or 201 on success; 403 on denial; 404 only when
// resolveTargetPath rejects the path (e.g. empty email gets 403 from
// the decider, not 404).
func TestInvariant_ZddcPutMatrix(t *testing.T) {
type principal struct {
email string
elevated bool
}
rootAdminElevated := principal{"admin@example.com", true}
rootAdminUnelevated := principal{"admin@example.com", false}
subtreeAdminElevated := principal{"alice@example.com", true}
subtreeAdminUnelevated := principal{"alice@example.com", false}
nonAdmin := principal{"eve@example.com", true}
anon := principal{"", true}
const (
ok = http.StatusOK
den = http.StatusForbidden
)
cases := []struct {
name string
target string
who principal
want int
}{
// Root .zddc
{"root admin elevated → root .zddc", "/.zddc", rootAdminElevated, ok},
{"root admin un-elevated → root .zddc", "/.zddc", rootAdminUnelevated, den},
{"subtree admin elevated → root .zddc", "/.zddc", subtreeAdminElevated, den},
{"subtree admin un-elevated → root .zddc", "/.zddc", subtreeAdminUnelevated, den},
{"non-admin → root .zddc", "/.zddc", nonAdmin, den},
{"anonymous → root .zddc", "/.zddc", anon, den},
// Project .zddc (no on-disk file yet — PUT creates it)
{"root admin elevated → project .zddc", "/Project-1/.zddc", rootAdminElevated, http.StatusCreated},
{"root admin un-elevated → project .zddc", "/Project-1/.zddc", rootAdminUnelevated, den},
{"subtree admin elevated (out-of-scope) → project .zddc", "/Project-1/.zddc", subtreeAdminElevated, den},
{"non-admin → project .zddc", "/Project-1/.zddc", nonAdmin, den},
// Subtree .zddc (alice administers this subtree)
{"root admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminElevated, ok},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, den},
{"subtree admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminElevated, ok},
{"subtree admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, den},
{"non-admin → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", nonAdmin, den},
{"anonymous → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", anon, den},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg, _ := invariantsFixture(t)
body := []byte("title: matrix probe\n")
rec := doReq(cfg, http.MethodPut, tc.target, tc.who.email, tc.who.elevated, body, "")
if tc.want == den {
if rec.Code != http.StatusForbidden && rec.Code != http.StatusUnauthorized {
t.Fatalf("want denial, got %d body=%s", rec.Code, dumpBody(rec))
}
} else if rec.Code != tc.want {
t.Fatalf("want %d, got %d body=%s", tc.want, rec.Code, dumpBody(rec))
}
})
}
}
// TestInvariant_ZddcDeleteMatrix mirrors ZddcPutMatrix for DELETE. The
// project-level .zddc target is dropped (no on-disk file → 404 lives
// outside the auth surface). The cases that remain pin: only an
// elevated admin with authority over the .zddc's directory can drop
// the file.
func TestInvariant_ZddcDeleteMatrix(t *testing.T) {
type principal struct {
email string
elevated bool
}
rootAdminElevated := principal{"admin@example.com", true}
rootAdminUnelevated := principal{"admin@example.com", false}
subtreeAdminElevated := principal{"alice@example.com", true}
subtreeAdminUnelevated := principal{"alice@example.com", false}
nonAdmin := principal{"eve@example.com", true}
cases := []struct {
name string
target string
who principal
want int
}{
{"root admin elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminElevated, http.StatusNoContent},
{"root admin un-elevated → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", rootAdminUnelevated, http.StatusForbidden},
{"subtree admin elevated → own .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminElevated, http.StatusNoContent},
{"subtree admin un-elevated → own .zddc", "/Project-1/archive/Acme/working/.zddc", subtreeAdminUnelevated, http.StatusForbidden},
{"non-admin → subtree .zddc", "/Project-1/archive/Acme/working/.zddc", nonAdmin, http.StatusForbidden},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg, _ := invariantsFixture(t)
rec := doReq(cfg, http.MethodDelete, tc.target, tc.who.email, tc.who.elevated, nil, "")
if rec.Code != tc.want {
t.Fatalf("want %d, got %d body=%s", tc.want, rec.Code, dumpBody(rec))
}
})
}
}
// ── Invariant 11 — anti-bypass: un-elevated admin gets nothing extra ──────
// TestInvariant_UnelevatedAdminNoSilentBypass is the anti-test for the
// elevation gate. For every (admin-flavour × action) tuple, an
// un-elevated admin must behave exactly like a non-admin: they may
// only do what an explicit ACL grant permits. The fixture's admin and
// alice both have NO baseline ACL grant outside their admin scope, so
// every action below MUST 403 — any pass indicates a bypass leak.
func TestInvariant_UnelevatedAdminNoSilentBypass(t *testing.T) {
cfg, _ := invariantsFixture(t)
type op struct {
method string
path string
body []byte
op string
}
probes := []op{
// .zddc writes (ActionAdmin)
{http.MethodPut, "/.zddc", []byte("title: x\n"), ""},
{http.MethodPut, "/Project-1/archive/Acme/working/.zddc", []byte("title: x\n"), ""},
{http.MethodDelete, "/Project-1/archive/Acme/working/.zddc", nil, ""},
// WORM writes (ActionWrite / ActionCreate stripped)
{http.MethodPut, "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md", []byte("# mutate\n"), ""},
{http.MethodPut, "/Project-1/archive/Acme/received/Acme-0042/new.pdf", []byte("%PDF\n"), ""},
{http.MethodDelete, "/Project-1/archive/Acme/issued/2026-05-15_Acme-0099 (IFR) - Test/Acme-0099_0A (IFR) - Test.md", nil, ""},
// Regular write into someone else's working/ home (no ACL grant)
{http.MethodPut, "/Project-1/archive/Acme/working/eve@example.com/draft.md", []byte("# steal\n"), ""},
}
admins := []struct {
name string
email string
}{
{"root super-admin", "admin@example.com"},
{"subtree admin (alice)", "alice@example.com"},
}
for _, a := range admins {
for _, p := range probes {
t.Run(a.name+" "+p.method+" "+p.path, func(t *testing.T) {
rec := doReq(cfg, p.method, p.path, a.email, false, p.body, p.op)
if rec.Code != http.StatusForbidden {
t.Fatalf("BYPASS LEAK: %s un-elevated reached %s %s with status %d body=%s",
a.email, p.method, p.path, rec.Code, dumpBody(rec))
}
})
}
}
}
// ── Invariant 9 — Profile admin endpoints 404 (not 403) for non-admins ────
func TestInvariant_ProfileAdminEndpointsHideFromNonAdmins(t *testing.T) {
// These checks lock in the existence-hiding property: non-admins must
// see 404, never 403, so they can't probe which paths exist.
t.Skip("requires the profile handler dispatcher entry point; skip until the refactor confirms ServeProfile signature")
}
// dump prints the rec body when t.Logf would help debugging — used in
// failure messages to avoid silently empty 403 cases.
func dumpBody(rec *httptest.ResponseRecorder) string {
s := rec.Body.String()
return strings.TrimSpace(s)
}