ZDDC/zddc/internal/handler/auth_invariants_test.go
ZDDC 3ac53fe894 fix(fileapi): authorize creates at the logical parent, not the nearest on-disk dir
authorizeAction walked `probe` up from the target's parent to the nearest
EXISTING directory before computing the ACL chain. For a create deep under a
not-yet-materialised canonical path — e.g. mkdir working/<party>/<name> when
working/ and working/<party>/ don't exist on disk yet — that walk skipped the
virtual working/ level and landed on the project root, where the embedded
grant is only `document_controller: rw` (no `c`). Result: a bona-fide
document_controller got 403 missing_verb=c creating in working/ (and party
registration would fail the same way on a fresh project where ssr/ doesn't
exist yet).

EffectivePolicy is virtual-path-aware — the paths: cascade resolves per-folder
behaviour for directories that don't exist on disk — so the chain must be
evaluated at filepath.Dir(absPath) directly. This applies the correct
per-peer grant (working/ → document_controller rwcda, project_team cr; ssr/ →
document_controller rwc) regardless of what's been physically created. Ancestor
restrictions (WORM zones, inherit:false fences) still apply because they cascade
through EffectivePolicy, so this is strictly more correct, never more permissive
than the cascade intends.

Regression test: a document_controller (role member, not admin, un-elevated)
registers a party and mkdirs under working/<party>/.

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

543 lines
23 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)
}
}
// 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 /<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 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)
}
// Regression: "I'm a document_controller but creating a folder in working/
// says I need document-controller permissions." A DC (role member at the site
// root, NOT an admin, un-elevated) must be able to (1) register a party by
// creating ssr/<party>.yaml and (2) create folders under working/<party>/,
// per the embedded per-peer grants (ssr → document_controller rwc; working →
// document_controller rwcda). Exercises role resolution from a deep peer level
// back to the root role definition.
func TestInvariant_DocumentControllerRegistersPartyAndCreatesInWorking(t *testing.T) {
cfg, _ := invariantsFixture(t)
// 1. Register a new party: create ssr/<party>.yaml.
rec := doReq(cfg, http.MethodPut, "/Project-1/ssr/Beta.yaml", "bob@example.com", false, []byte("kind: SSR\n"), "")
if rec.Code != http.StatusCreated && rec.Code != http.StatusOK {
t.Fatalf("DC register party ssr/Beta.yaml: status=%d body=%s (want 201/200)", rec.Code, rec.Body.String())
}
zddc.InvalidateCache(cfg.Root)
// 2. Create a folder under working/<party>/.
rec2 := doReq(cfg, http.MethodPost, "/Project-1/working/Beta/draft/", "bob@example.com", false, nil, "mkdir")
if rec2.Code != http.StatusCreated && rec2.Code != http.StatusOK {
t.Fatalf("DC mkdir working/Beta/draft: status=%d body=%s (want 201/200)", rec2.Code, rec2.Body.String())
}
}