mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-17 05:37:43 +00:00
fix: force include_usage, auth event logging, credential-strip coverage
C1 (quota bypass): a client sending stream_options.include_usage=false
suppressed upstream usage reporting and escaped every token/cost budget,
since the non-streaming T12 estimate never covers a streaming response.
chatCompletion (provider_openai.go) now always forces include_usage=true
on the outgoing request, merged into a copy of the client's own
stream_options map so the caller's map is never mutated in place.
clientAskedUsage is derived from the client's own value via isTruthy, so
the usage-only chunk is still suppressed for a client that never asked
for it.
I1 (vacuous security tests): the X-Api-Key credential-strip set had zero
effective coverage — existing tests either never sent x-api-key, or
targeted anthropic, whose injectAuth overwrites x-api-key regardless of
whether the strip ran. Added one openai-passthrough and one MCP-proxy
test that authenticate via x-api-key itself and assert it arrives empty
upstream. Mutation-checked: both fail when "X-Api-Key": true is removed
from gatewayCredentialHeaders (verified, then restored).
I3 (auth-event logging): added Gateway.logAuthEvent (logger.go), called
at all six authStore.identify call sites (llmgateway.go's ServeHTTP,
routes_unified.go's handleModels). A failed attempt logs method, path,
and remote address; a success logs the resolved user's name and route.
Neither line ever includes the presented API key.
M2 (usage.estimated): runUnified now reads result.estimated and logs the
estimate explicitly when a non-streaming response's usage falls back to
the body-size heuristic, making the field genuinely read — removes the
stale //nolint:unused on limits.go's usage struct.
M8 (gosec consistency): secrets.go's file: secret path read now carries
the same "#nosec G304 -- operator-supplied file path from middleware
config" annotation as users_file.go's equivalent read.
Doc nit: mcp_a2a.go's validateTargetURLs comment now also names the
dot-only ("."/"..") rule validateConfigName enforces, not just character
set and reserved names.
Gates: go test ./... -count=1 (-race) green, gofmt -l . empty, go vet
clean, make yaegi-check OK, golangci-lint (gosec+staticcheck+unused+
errcheck+govet, global config) 0 issues.
This commit is contained in:
@@ -176,6 +176,95 @@ func TestGateway_Logf_WritesInfoPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeHTTP_AuthFailed_LogsMethodPathNotKey proves a failed
|
||||
// authentication attempt (I3) emits a log line naming the method and path,
|
||||
// and proves the presented (wrong) API key never appears in that line —
|
||||
// key material must never be logged, even on a failure.
|
||||
func TestServeHTTP_AuthFailed_LogsMethodPathNotKey(t *testing.T) {
|
||||
const presentedKey = "sk-wrong-should-never-be-logged"
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
cfg := CreateConfig()
|
||||
cfg.Providers = map[string]*ProviderConfig{"openai": {Type: "openai", APIKey: "k"}}
|
||||
cfg.Groups = map[string]*GroupConfig{"default": {}}
|
||||
cfg.Users = &UsersConfig{Inline: []*UserConfig{{Name: "alice", Group: "default", APIKey: "sk-alice"}}}
|
||||
h, err := New(context.Background(), next, cfg, "llmgw")
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
origStderr := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{}`))
|
||||
req.Header.Set("Authorization", "Bearer "+presentedKey)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
_ = w.Close() // closing the pipe write end to unblock the read; error not actionable in a test
|
||||
os.Stderr = origStderr
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, r); err != nil {
|
||||
t.Fatalf("io.Copy: %v", err)
|
||||
}
|
||||
logged := buf.String()
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(logged, "auth failed") || !strings.Contains(logged, "POST") || !strings.Contains(logged, "/v1/chat/completions") {
|
||||
t.Fatalf("want stderr to contain an auth-failed line with method and path, got %q", logged)
|
||||
}
|
||||
if strings.Contains(logged, presentedKey) {
|
||||
t.Fatalf("stderr contains the presented API key %q — key material must never be logged: %q", presentedKey, logged)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeHTTP_AuthOK_LogsUserNameAndRoute proves a successful
|
||||
// authentication attempt logs the resolved user's name and the route,
|
||||
// without ever logging the API key that authenticated them.
|
||||
func TestServeHTTP_AuthOK_LogsUserNameAndRoute(t *testing.T) {
|
||||
const presentedKey = "sk-alice-should-never-be-logged"
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
cfg := CreateConfig()
|
||||
cfg.Providers = map[string]*ProviderConfig{"openai": {Type: "openai", APIKey: "k"}}
|
||||
cfg.Groups = map[string]*GroupConfig{"default": {}}
|
||||
cfg.Users = &UsersConfig{Inline: []*UserConfig{{Name: "alice", Group: "default", APIKey: presentedKey}}}
|
||||
h, err := New(context.Background(), next, cfg, "llmgw")
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
origStderr := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stderr = w
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+presentedKey)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
_ = w.Close() // closing the pipe write end to unblock the read; error not actionable in a test
|
||||
os.Stderr = origStderr
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, r); err != nil {
|
||||
t.Fatalf("io.Copy: %v", err)
|
||||
}
|
||||
logged := buf.String()
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(logged, "auth ok") || !strings.Contains(logged, `"alice"`) || !strings.Contains(logged, "/v1/models") {
|
||||
t.Fatalf("want stderr to contain an auth-ok line naming user alice and route /v1/models, got %q", logged)
|
||||
}
|
||||
if strings.Contains(logged, presentedKey) {
|
||||
t.Fatalf("stderr contains the presented API key %q — key material must never be logged: %q", presentedKey, logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateway_Errorf_WritesErrorPrefix(t *testing.T) {
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
cfg := CreateConfig()
|
||||
|
||||
Reference in New Issue
Block a user