diff --git a/internal/api/client.go b/internal/api/client.go index 7cc5a4b..82ecd84 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -35,7 +35,12 @@ type debugTransport struct { } func (dt *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { - dump, _ := httputil.DumpRequestOut(req, true) + safeReq := req.Clone(req.Context()) + if auth := safeReq.Header.Get("Authorization"); auth != "" { + safeReq.Header.Set("Authorization", "Bearer [REDACTED]") + } + safeReq.Body = http.NoBody + dump, _ := httputil.DumpRequestOut(safeReq, false) fmt.Fprintf(os.Stderr, "\n--- DEBUG REQUEST ---\n%s\n", dump) resp, err := dt.transport.RoundTrip(req) diff --git a/internal/api/client_test.go b/internal/api/client_test.go index e46a526..db50d74 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -1,10 +1,14 @@ package api import ( + "bytes" "context" "encoding/json" + "io" "net/http" "net/http/httptest" + "os" + "strings" "testing" "github.com/rootlyhq/rootly-cli/internal/config" @@ -221,3 +225,115 @@ func TestParseTimePtr(t *testing.T) { func strPtr(s string) *string { return &s } + +func TestDebugTransportRedactsAuthorization(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + var buf bytes.Buffer + dt := &debugTransport{transport: http.DefaultTransport} + + req, err := http.NewRequestWithContext(context.Background(), "GET", server.URL+"/v1/incidents", http.NoBody) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", "Bearer super-secret-token-12345") + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + resp, err := dt.RoundTrip(req) + if err != nil { + t.Fatalf("RoundTrip failed: %v", err) + } + resp.Body.Close() + + w.Close() + io.Copy(&buf, r) + os.Stderr = oldStderr + + output := buf.String() + if strings.Contains(output, "super-secret-token-12345") { + t.Error("debug output contains raw API token — Authorization header must be redacted") + } + if !strings.Contains(output, "Bearer [REDACTED]") { + t.Error("debug output should contain 'Bearer [REDACTED]'") + } + if !strings.Contains(output, "DEBUG REQUEST") { + t.Error("debug output should contain request dump marker") + } +} + +func TestDebugTransportPreservesActualAuthHeader(t *testing.T) { + var receivedAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + dt := &debugTransport{transport: http.DefaultTransport} + + req, err := http.NewRequestWithContext(context.Background(), "GET", server.URL+"/v1/incidents", http.NoBody) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", "Bearer real-token") + + oldStderr := os.Stderr + _, w, _ := os.Pipe() + os.Stderr = w + + resp, err := dt.RoundTrip(req) + w.Close() + os.Stderr = oldStderr + + if err != nil { + t.Fatalf("RoundTrip failed: %v", err) + } + resp.Body.Close() + + if receivedAuth != "Bearer real-token" { + t.Errorf("server received Authorization = %q, want %q", receivedAuth, "Bearer real-token") + } +} + +func TestDebugTransportNoAuthHeader(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + var buf bytes.Buffer + dt := &debugTransport{transport: http.DefaultTransport} + + req, err := http.NewRequestWithContext(context.Background(), "GET", server.URL+"/v1/incidents", http.NoBody) + if err != nil { + t.Fatal(err) + } + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + resp, err := dt.RoundTrip(req) + if err != nil { + t.Fatalf("RoundTrip failed: %v", err) + } + resp.Body.Close() + + w.Close() + io.Copy(&buf, r) + os.Stderr = oldStderr + + output := buf.String() + if strings.Contains(output, "[REDACTED]") { + t.Error("should not contain [REDACTED] when no Authorization header is set") + } + if !strings.Contains(output, "DEBUG REQUEST") { + t.Error("debug output should still contain request dump marker") + } +}