feat(server): honor ?admin=true|false elevation on every endpoint
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) <noreply@anthropic.com>
This commit is contained in:
parent
9513ea3a07
commit
c59bea183e
2 changed files with 109 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue