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.
|
// named in admin lists.
|
||||||
const elevationCookieName = "zddc-elevate"
|
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
|
// ACLMiddleware extracts the user email and stores it (along with the
|
||||||
// policy decider) in the request context. It does NOT enforce ACL
|
// policy decider) in the request context. It does NOT enforce ACL
|
||||||
// itself — each handler performs its own ACL check via
|
// 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" {
|
if c, err := r.Cookie(elevationCookieName); err == nil && c.Value == "1" {
|
||||||
elevated = true
|
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
|
// DEBUG-level header dump for diagnosing proxy / SSO header
|
||||||
// passthrough. Off by default (LogLevel info); enable with
|
// passthrough. Off by default (LogLevel info); enable with
|
||||||
|
|
|
||||||
|
|
@ -194,12 +194,12 @@ func TestAccessLog_ChainAdminLevelAttribution(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
email string
|
email string
|
||||||
elevate bool
|
elevate bool
|
||||||
path string
|
path string
|
||||||
wantLevel int
|
wantLevel int
|
||||||
wantActive bool
|
wantActive bool
|
||||||
}{
|
}{
|
||||||
{"root admin elevated probing root → level 0", "root@example.com", true, "/", 0, true},
|
{"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},
|
{"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