Skip to content

Commit 9119d86

Browse files
julianknutsenclaude
andcommitted
fix: ensure impersonation always goes through authenticated path
Two issues caused impersonation to silently fall back to the anonymous public client (empty mode, no actions): 1. Auth middleware: when X-Wasteland header was missing (race condition, impersonation without explicit upstream), multi-wasteland users hit passOrBlock which let GET requests through without auth context. Now GET requests always default to the first upstream. 2. Cache-Control: hosted mode used "public" which let browsers cache responses across auth states. Changed to "private" so per-user responses are never served from a shared cache. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1eacbab commit 9119d86

2 files changed

Lines changed: 15 additions & 4 deletions

File tree

internal/api/handlers.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (s *Server) handleBrowse(w http.ResponseWriter, r *http.Request) {
7171
return
7272
}
7373
w.Header().Set("Content-Type", "application/json")
74-
w.Header().Set("Cache-Control", "public, max-age=15, stale-while-revalidate=30")
74+
w.Header().Set("Cache-Control", s.cacheControl())
7575
w.WriteHeader(http.StatusOK)
7676
if _, err := w.Write(data); err != nil {
7777
slog.Warn("failed to write browse response", "error", err)
@@ -113,13 +113,22 @@ func (s *Server) handleDetail(w http.ResponseWriter, r *http.Request) {
113113
return
114114
}
115115
w.Header().Set("Content-Type", "application/json")
116-
w.Header().Set("Cache-Control", "public, max-age=15, stale-while-revalidate=30")
116+
w.Header().Set("Cache-Control", s.cacheControl())
117117
w.WriteHeader(http.StatusOK)
118118
if _, err := w.Write(data); err != nil {
119119
slog.Warn("failed to write detail response", "error", err)
120120
}
121121
}
122122

123+
// cacheControl returns the appropriate Cache-Control header value.
124+
// Hosted mode uses private caching since responses vary per user.
125+
func (s *Server) cacheControl() string {
126+
if s.hosted {
127+
return "private, max-age=15, stale-while-revalidate=30"
128+
}
129+
return "public, max-age=15, stale-while-revalidate=30"
130+
}
131+
123132
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
124133
client, ok := s.resolveClient(w, r)
125134
if !ok {

internal/hosted/auth.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
113113
upstream := r.Header.Get("X-Wasteland")
114114
upstreams := workspace.Upstreams()
115115

116-
if upstream == "" && len(upstreams) == 1 {
117-
// Single-wasteland fallback for backward compat.
116+
if upstream == "" && len(upstreams) > 0 && r.Method == http.MethodGet {
117+
// Default to first upstream for reads when header is missing
118+
// (race with frontend context init, impersonation, or
119+
// single-wasteland backward compat).
118120
upstream = upstreams[0].Upstream
119121
}
120122

0 commit comments

Comments
 (0)