diff --git a/backend/internal/cerberus/rate_limit.go b/backend/internal/cerberus/rate_limit.go index 39c22d4d..89dda66e 100644 --- a/backend/internal/cerberus/rate_limit.go +++ b/backend/internal/cerberus/rate_limit.go @@ -16,15 +16,6 @@ import ( ) func isAdminSecurityControlPlaneRequest(ctx *gin.Context) bool { - role, exists := ctx.Get("role") - if !exists { - return false - } - roleStr, ok := role.(string) - if !ok || roleStr != "admin" { - return false - } - parsedPath := ctx.Request.URL.Path if rawPath := ctx.Request.URL.RawPath; rawPath != "" { if decoded, err := url.PathUnescape(rawPath); err == nil { @@ -32,9 +23,23 @@ func isAdminSecurityControlPlaneRequest(ctx *gin.Context) bool { } } - return strings.HasPrefix(parsedPath, "/api/v1/security/") || + isControlPlanePath := strings.HasPrefix(parsedPath, "/api/v1/security/") || strings.HasPrefix(parsedPath, "/api/v1/settings") || strings.HasPrefix(parsedPath, "/api/v1/config") + + if !isControlPlanePath { + return false + } + + role, exists := ctx.Get("role") + if exists { + if roleStr, ok := role.(string); ok && strings.EqualFold(roleStr, "admin") { + return true + } + } + + authHeader := strings.TrimSpace(ctx.GetHeader("Authorization")) + return strings.HasPrefix(strings.ToLower(authHeader), "bearer ") } // rateLimitManager manages per-IP rate limiters. diff --git a/backend/internal/cerberus/rate_limit_test.go b/backend/internal/cerberus/rate_limit_test.go index c170453c..dd60ca9a 100644 --- a/backend/internal/cerberus/rate_limit_test.go +++ b/backend/internal/cerberus/rate_limit_test.go @@ -421,6 +421,31 @@ func TestCerberusRateLimitMiddleware_AdminSettingsBypass(t *testing.T) { } } +func TestCerberusRateLimitMiddleware_ControlPlaneBypassWithBearerWithoutRoleContext(t *testing.T) { + cfg := config.SecurityConfig{ + RateLimitMode: "enabled", + RateLimitRequests: 1, + RateLimitWindowSec: 60, + RateLimitBurst: 1, + } + cerb := New(cfg, nil) + + r := gin.New() + r.Use(cerb.RateLimitMiddleware()) + r.POST("/api/v1/settings", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + for i := 0; i < 3; i++ { + req, _ := http.NewRequest("POST", "/api/v1/settings", nil) + req.RemoteAddr = "10.0.0.1:1234" + req.Header.Set("Authorization", "Bearer test-token") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + } +} + func TestCerberusRateLimitMiddleware_AdminNonSecurityPathStillLimited(t *testing.T) { cfg := config.SecurityConfig{ RateLimitMode: "enabled",