From c59bea183e237ea1c4a2c813654c7468ae34cb75 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 4 Jun 2026 13:13:30 -0500 Subject: [PATCH] feat(server): honor ?admin=true|false elevation on every endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shared/elevation.js toggles admin mode via the ?admin= URL param, but it's client-side JS — it only runs on HTML tool pages, where it sets the sticky zddc-elevate cookie. A raw endpoint (a directory's JSON listing, zip browsing at /<…>.zip/, the file API) loads no JS, so ?admin=true was inert there and such requests stayed un-elevated. ACLMiddleware now reads the same ?admin= toggle directly: true|1|on|yes elevates the request, false|0|off|no drops it (overriding the cookie for that request). This is per-request only — the server doesn't set/clear the cookie; elevation.js still owns sticky persistence on pages. Elevation grants powers only to a caller who already holds admin authority (every admin call site re-checks via IsActiveAdmin), so a non-admin's ?admin=true sets the forensic flag but confers nothing. Makes e.g. GET /.zddc.zip/?admin=true work for an admin without first arming the cookie on a page. Co-Authored-By: Claude Opus 4.8 (1M context) --- zddc/internal/handler/middleware.go | 33 ++++++++++ zddc/internal/handler/middleware_test.go | 82 ++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/zddc/internal/handler/middleware.go b/zddc/internal/handler/middleware.go index b0bce30..fbbbb46 100644 --- a/zddc/internal/handler/middleware.go +++ b/zddc/internal/handler/middleware.go @@ -46,6 +46,25 @@ const ElevatedKey contextKey = "elevated" // named in admin lists. const elevationCookieName = "zddc-elevate" +// adminQueryParam reads the ?admin= elevation toggle, returning a pointer to +// the requested state (true = elevate, false = drop) or nil when the param is +// absent or unrecognised. Recognised values mirror shared/elevation.js so the +// URL toggle behaves identically whether elevation.js sets the cookie or the +// server honors the bare param: true/1/on/yes and false/0/off/no +// (case-insensitive). +func adminQueryParam(r *http.Request) *bool { + v := strings.ToLower(r.URL.Query().Get("admin")) + switch v { + case "true", "1", "on", "yes": + t := true + return &t + case "false", "0", "off", "no": + f := false + return &f + } + return nil +} + // ACLMiddleware extracts the user email and stores it (along with the // policy decider) in the request context. It does NOT enforce ACL // itself — each handler performs its own ACL check via @@ -98,6 +117,20 @@ func ACLMiddleware(cfg config.Config, decider policy.Decider, tokens *auth.Store if c, err := r.Cookie(elevationCookieName); err == nil && c.Value == "1" { elevated = true } + // ?admin=true|1|on|yes elevates this request directly, and + // ?admin=false|0|off|no drops it — mirroring the URL toggle in + // shared/elevation.js, but honored at the server so the param + // works on EVERY endpoint (raw directory listings, zip browsing, + // the file API), not just HTML pages where elevation.js runs to + // set the cookie. elevation.js still sets the cookie for sticky + // persistence across navigation; this just makes the bare param + // effective on a single direct request too. Elevation only grants + // powers to a caller who already holds admin authority (every + // admin call site re-checks the cascade via IsActiveAdmin), so + // honoring the param for a non-admin is a harmless no-op. + if v := adminQueryParam(r); v != nil { + elevated = *v + } } // DEBUG-level header dump for diagnosing proxy / SSO header // passthrough. Off by default (LogLevel info); enable with diff --git a/zddc/internal/handler/middleware_test.go b/zddc/internal/handler/middleware_test.go index 0474507..99c68e7 100644 --- a/zddc/internal/handler/middleware_test.go +++ b/zddc/internal/handler/middleware_test.go @@ -194,12 +194,12 @@ func TestAccessLog_ChainAdminLevelAttribution(t *testing.T) { } cases := []struct { - name string - email string - elevate bool - path string - wantLevel int - wantActive bool + name string + email string + elevate bool + path string + wantLevel int + wantActive bool }{ {"root admin elevated probing root → level 0", "root@example.com", true, "/", 0, true}, {"root admin elevated probing project → level 0 (walks down chain)", "root@example.com", true, "/Project-1/", 0, true}, @@ -250,3 +250,73 @@ func TestAccessLog_ChainAdminLevelAttribution(t *testing.T) { }) } } + +// TestACLMiddleware_AdminQueryParamElevation verifies the server honors the +// ?admin= URL toggle directly (mirroring shared/elevation.js), so the param +// elevates ANY endpoint — not just HTML pages where elevation.js runs to set +// the cookie. ?admin=true elevates with no cookie; ?admin=false drops even +// when the cookie is present; a non-admin's ?admin=true sets the flag but +// confers no authority. +func TestACLMiddleware_AdminQueryParamElevation(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, ".zddc"), + []byte("admins:\n - root@example.com\n"), 0o644); err != nil { + t.Fatalf("write root .zddc: %v", err) + } + zddc.InvalidateCache(root) + + cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"} + noop := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) + + type record struct { + Elevated bool `json:"elevated"` + ActiveAdmin bool `json:"active_admin"` + } + run := func(t *testing.T, path, email string, cookie bool) record { + t.Helper() + var buf bytes.Buffer + auditLogger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})) + chain := ACLMiddleware(cfg, nil, nil, AccessLogMiddleware(cfg, auditLogger, noop)) + req := httptest.NewRequest(http.MethodGet, path, nil) + if email != "" { + req.Header.Set("X-Auth-Request-Email", email) + } + if cookie { + req.AddCookie(&http.Cookie{Name: "zddc-elevate", Value: "1"}) + } + chain.ServeHTTP(httptest.NewRecorder(), req) + var rec record + if err := json.Unmarshal(buf.Bytes(), &rec); err != nil { + t.Fatalf("audit log not JSON: %v; raw=%s", err, buf.String()) + } + return rec + } + + t.Run("?admin=true elevates root admin with no cookie", func(t *testing.T) { + rec := run(t, "/?admin=true", "root@example.com", false) + if !rec.Elevated || !rec.ActiveAdmin { + t.Errorf("elevated=%v active=%v, want both true", rec.Elevated, rec.ActiveAdmin) + } + }) + t.Run("?admin=false drops despite cookie", func(t *testing.T) { + rec := run(t, "/?admin=false", "root@example.com", true) + if rec.Elevated || rec.ActiveAdmin { + t.Errorf("elevated=%v active=%v, want both false", rec.Elevated, rec.ActiveAdmin) + } + }) + t.Run("non-admin ?admin=true sets flag but confers no authority", func(t *testing.T) { + rec := run(t, "/?admin=true", "stranger@example.com", false) + if !rec.Elevated { + t.Errorf("elevated=%v, want true (flag set)", rec.Elevated) + } + if rec.ActiveAdmin { + t.Errorf("active_admin=%v, want false (no admin authority)", rec.ActiveAdmin) + } + }) + t.Run("no param, no cookie → not elevated", func(t *testing.T) { + rec := run(t, "/", "root@example.com", false) + if rec.Elevated || rec.ActiveAdmin { + t.Errorf("elevated=%v active=%v, want both false", rec.Elevated, rec.ActiveAdmin) + } + }) +}