ZDDC/zddc/internal/handler/cors_test.go
ZDDC 9ef90800b1 feat(zddc-server): admin debug page + X-Auth-Request-Email default + hidden-segment guard
Three improvements bundled because they all ship as zddc-server v0.0.2:

* /.admin/ debug dashboard with /whoami, /config, /logs sub-routes.
  Authorization via a top-level `admins:` glob list in <ZDDC_ROOT>/.zddc
  (root-only — subdir entries deliberately ignored to prevent privilege
  escalation via subtree write access). Non-admin requests get 404 so the
  page is invisible. Recent logs surface via a 500-entry slog ring buffer
  teed off the existing TextHandler. Lets operators debug without
  kubectl exec.

* Default ZDDC_EMAIL_HEADER changes from `X-Email` to
  `X-Auth-Request-Email` — the oauth2-proxy / nginx auth-request
  convention that the TND helm chart already sets explicitly.
  Operators who set the env var explicitly are unaffected; deployments
  relying on the previous default need to set ZDDC_EMAIL_HEADER=X-Email
  or update their proxy.

* dispatch() rejects any URL whose segments contain a dot prefix other
  than the recognized virtual prefixes (.admin, cfg.IndexPath /
  .archive). Matches the existing listing-pipeline filter so hidden
  subtrees on the served PVC (e.g. /srv/.devshell — used by the
  in-cluster dev-shell for persistent home-dir state) become
  unreachable via direct HTTP fetch, not just hidden in listings.

Refreshes the X-Email reference in website/index.html accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:02:06 -05:00

140 lines
4 KiB
Go

package handler
import (
"net/http"
"net/http/httptest"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
)
func TestCORSMiddleware(t *testing.T) {
allowed := config.Config{CORSOrigins: []string{"https://zddc.varasys.io", "https://other.example"}}
disabled := config.Config{CORSOrigins: nil}
pass := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
cases := []struct {
name string
cfg config.Config
method string
origin string
acrHeaders string
wantStatus int
wantAllowOrig string
wantAllowCreds string
wantVary string
wantAllowHdrs string
wantNextCalled bool
}{
{
name: "allowed origin, GET",
cfg: allowed,
method: http.MethodGet,
origin: "https://zddc.varasys.io",
wantStatus: http.StatusOK,
wantAllowOrig: "https://zddc.varasys.io",
wantAllowCreds: "true",
wantVary: "Origin",
wantNextCalled: true,
},
{
name: "second allowed origin, GET",
cfg: allowed,
method: http.MethodGet,
origin: "https://other.example",
wantStatus: http.StatusOK,
wantAllowOrig: "https://other.example",
wantAllowCreds: "true",
wantVary: "Origin",
wantNextCalled: true,
},
{
name: "disallowed origin, GET",
cfg: allowed,
method: http.MethodGet,
origin: "https://evil.example",
wantStatus: http.StatusOK,
wantNextCalled: true,
},
{
name: "no Origin header, GET",
cfg: allowed,
method: http.MethodGet,
wantStatus: http.StatusOK,
wantNextCalled: true,
},
{
name: "OPTIONS preflight, allowed origin",
cfg: allowed,
method: http.MethodOptions,
origin: "https://zddc.varasys.io",
acrHeaders: "X-Auth-Request-Email, Content-Type",
wantStatus: http.StatusNoContent,
wantAllowOrig: "https://zddc.varasys.io",
wantAllowCreds: "true",
wantVary: "Origin",
wantAllowHdrs: "X-Auth-Request-Email, Content-Type",
wantNextCalled: false,
},
{
name: "OPTIONS preflight, disallowed origin falls through",
cfg: allowed,
method: http.MethodOptions,
origin: "https://evil.example",
wantStatus: http.StatusOK,
wantNextCalled: true,
},
{
name: "CORS disabled passes through",
cfg: disabled,
method: http.MethodGet,
origin: "https://zddc.varasys.io",
wantStatus: http.StatusOK,
wantNextCalled: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
called := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
pass.ServeHTTP(w, r)
})
h := CORSMiddleware(tc.cfg, next)
req := httptest.NewRequest(tc.method, "/", nil)
if tc.origin != "" {
req.Header.Set("Origin", tc.origin)
}
if tc.acrHeaders != "" {
req.Header.Set("Access-Control-Request-Headers", tc.acrHeaders)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != tc.wantStatus {
t.Errorf("status = %d, want %d", rec.Code, tc.wantStatus)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != tc.wantAllowOrig {
t.Errorf("Access-Control-Allow-Origin = %q, want %q", got, tc.wantAllowOrig)
}
if got := rec.Header().Get("Access-Control-Allow-Credentials"); got != tc.wantAllowCreds {
t.Errorf("Access-Control-Allow-Credentials = %q, want %q", got, tc.wantAllowCreds)
}
if got := rec.Header().Get("Vary"); got != tc.wantVary {
t.Errorf("Vary = %q, want %q", got, tc.wantVary)
}
if got := rec.Header().Get("Access-Control-Allow-Headers"); got != tc.wantAllowHdrs {
t.Errorf("Access-Control-Allow-Headers = %q, want %q", got, tc.wantAllowHdrs)
}
if called != tc.wantNextCalled {
t.Errorf("next called = %v, want %v", called, tc.wantNextCalled)
}
})
}
}