Major refactoring to hexagonal (ports & adapters) architecture: - Add service layer (apikey_service, project_service) for business logic - Add webhook system with dispatcher and delivery tracking - Add command queue with priority-based processing - Add rate limiting with sliding window algorithm - Add audit logging for command execution - Add OpenTelemetry integration (traces, metrics, spans) - Add circuit breaker for fault tolerance - Add cached repository wrapper for performance - Add comprehensive validation package - Add Kubernetes client integration for pod management - Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks) - Add network policy and PodDisruptionBudget for k8s - Remove legacy executor and projects/registry packages - Untrack secrets.yaml (now managed via envault) - Add coverage.out to .gitignore - Add e2e test infrastructure with docker-compose - Add comprehensive documentation (API, architecture, operations, plans) - Add golangci-lint config and pre-commit hook Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
294 lines
7.3 KiB
Go
294 lines
7.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// BenchmarkAuthMiddleware benchmarks the authentication middleware overhead.
|
|
func BenchmarkAuthMiddleware(b *testing.B) {
|
|
// Create a mock API key
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute, ScopeProjectsRead},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Simple handler that just writes OK
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Wrap with a mock middleware that simulates auth without DB
|
|
middleware := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Simulate auth header parsing
|
|
key := r.Header.Get(HeaderAPIKey)
|
|
if key == "" {
|
|
auth := r.Header.Get("Authorization")
|
|
if len(auth) > 7 && auth[:7] == "Bearer " {
|
|
key = auth[7:]
|
|
}
|
|
}
|
|
|
|
if key == "" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Simulate key validation (without DB)
|
|
if key != "valid-key" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Add key to context
|
|
ctx := context.WithValue(r.Context(), contextKeyAPIKey, apiKey)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
wrappedHandler := middleware(handler)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/projects", nil)
|
|
req.Header.Set(HeaderAPIKey, "valid-key")
|
|
rec := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(rec, req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkAuthMiddleware_Bearer benchmarks auth with Bearer token.
|
|
func BenchmarkAuthMiddleware_Bearer(b *testing.B) {
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute, ScopeProjectsRead},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
middleware := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
key := r.Header.Get(HeaderAPIKey)
|
|
if key == "" {
|
|
auth := r.Header.Get("Authorization")
|
|
if len(auth) > 7 && auth[:7] == "Bearer " {
|
|
key = auth[7:]
|
|
}
|
|
}
|
|
|
|
if key == "" || key != "valid-key" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), contextKeyAPIKey, apiKey)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
wrappedHandler := middleware(handler)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/projects", nil)
|
|
req.Header.Set("Authorization", "Bearer valid-key")
|
|
rec := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(rec, req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkRequireScope benchmarks the scope checking middleware.
|
|
func BenchmarkRequireScope(b *testing.B) {
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute, ScopeProjectsRead},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
scopeMiddleware := RequireScope(ScopeProjectsExecute)
|
|
wrappedHandler := scopeMiddleware(handler)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("POST", "/projects/test/claude", nil)
|
|
// Pre-set the API key in context
|
|
ctx := context.WithValue(req.Context(), contextKeyAPIKey, apiKey)
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(rec, req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkGetClientIP benchmarks IP extraction from requests.
|
|
func BenchmarkGetClientIP(b *testing.B) {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
req.RemoteAddr = "192.168.1.100:12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = getClientIP(req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkGetClientIP_XForwardedFor benchmarks IP extraction with X-Forwarded-For.
|
|
func BenchmarkGetClientIP_XForwardedFor(b *testing.B) {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
req.Header.Set("X-Forwarded-For", "10.0.0.1, 192.168.1.1, 172.16.0.1")
|
|
req.RemoteAddr = "127.0.0.1:12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = getClientIP(req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkGetClientIP_IPv6 benchmarks IP extraction for IPv6.
|
|
func BenchmarkGetClientIP_IPv6(b *testing.B) {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
req.RemoteAddr = "[2001:db8::1]:12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = getClientIP(req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkIPAllowlistCheck benchmarks the IP allowlist checking.
|
|
func BenchmarkIPAllowlistCheck(b *testing.B) {
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute},
|
|
AllowedIPs: []string{"192.168.1.0/24", "10.0.0.0/8", "172.16.0.0/12"},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = apiKey.IsIPAllowed("192.168.1.100")
|
|
}
|
|
}
|
|
|
|
// BenchmarkIPAllowlistCheck_NoAllowlist benchmarks IP check when no allowlist configured.
|
|
func BenchmarkIPAllowlistCheck_NoAllowlist(b *testing.B) {
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = apiKey.IsIPAllowed("192.168.1.100")
|
|
}
|
|
}
|
|
|
|
// BenchmarkHealthEndpointSkip benchmarks the path-skip logic for health endpoints.
|
|
func BenchmarkHealthEndpointSkip(b *testing.B) {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Simulate the skip check in middleware
|
|
middleware := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
if path == "/health" || path == "/ready" || path == "/metrics" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
// Would do auth here
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
wrappedHandler := middleware(handler)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
rec := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(rec, req)
|
|
}
|
|
}
|
|
|
|
// BenchmarkConcurrentAuth benchmarks concurrent auth middleware calls.
|
|
func BenchmarkConcurrentAuth(b *testing.B) {
|
|
apiKey := &APIKey{
|
|
ID: "test-key-id",
|
|
Name: "benchmark-key",
|
|
KeyPrefix: "rdev",
|
|
Scopes: []Scope{ScopeProjectsExecute, ScopeProjectsRead},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
middleware := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
key := r.Header.Get(HeaderAPIKey)
|
|
if key == "" || key != "valid-key" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
ctx := context.WithValue(r.Context(), contextKeyAPIKey, apiKey)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
wrappedHandler := middleware(handler)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
req := httptest.NewRequest("GET", "/projects", nil)
|
|
req.Header.Set(HeaderAPIKey, "valid-key")
|
|
rec := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(rec, req)
|
|
}
|
|
})
|
|
}
|