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) } } // Register the party (party_source: ssr) so writes under the // party_source peers aren't rejected before the WORM/admin checks // these invariants actually exercise. mustWriteHelper(t, filepath.Join(root, "Project-1/ssr/Acme.yaml"), "kind: SSR\n") // 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 //.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// 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// // 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 admin-only resources // exist. ServeProfile is the dispatcher (the refactor this test waited // on); its adminOnly wrapper denies with 404 before the sub-handler // runs, so a nil ring/index is safe for the non-admin paths. cfg, _ := invariantsFixture(t) adminEndpoints := []string{"/whoami", "/config", "/logs", "/effective-policy", "/reindex"} profileGet := func(sub, email string, elevated bool) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, ProfilePathPrefix+sub, nil) ctx := context.WithValue(req.Context(), EmailKey, email) ctx = context.WithValue(ctx, ElevatedKey, elevated) req = req.WithContext(ctx) rec := httptest.NewRecorder() ServeProfile(cfg, nil, nil, rec, req) return rec } // Non-admin (eve, project_team only) and anonymous callers must get // 404 on every admin endpoint — never 403, never 200. for _, who := range []struct { email string elevated bool label string }{ {"eve@example.com", false, "non-admin"}, {"", false, "anonymous"}, {"admin@example.com", false, "un-elevated admin"}, // sudo-style: no authority until elevated } { for _, sub := range adminEndpoints { rec := profileGet(sub, who.email, who.elevated) if rec.Code != http.StatusNotFound { t.Errorf("%s GET /.profile%s = %d, want 404 (existence-hiding)", who.label, sub, rec.Code) } } } // Positive control: an elevated root admin must NOT get 404 on the // gated routes that need no ring/index — proving the 404s above are // the admin gate, not a missing route. (/whoami and /config don't // touch the log ring or archive index.) for _, sub := range []string{"/whoami", "/config"} { rec := profileGet(sub, "admin@example.com", true) if rec.Code == http.StatusNotFound { t.Errorf("elevated admin GET /.profile%s = 404; the gate should admit admins", sub) } } } // 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) }