chore: git cache cleanup
This commit is contained in:
246
backend/internal/api/handlers/security_toggles_test.go
Normal file
246
backend/internal/api/handlers/security_toggles_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/caddy"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupToggleTest(t *testing.T) (*SecurityHandler, *gorm.DB) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{}))
|
||||
|
||||
// Create default SecurityConfig
|
||||
require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error)
|
||||
|
||||
cfg := config.SecurityConfig{}
|
||||
h := NewSecurityHandler(cfg, db, nil) // caddyManager nil to avoid reload logic
|
||||
return h, db
|
||||
}
|
||||
|
||||
func TestSecurityToggles(t *testing.T) {
|
||||
h, db := setupToggleTest(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
handler gin.HandlerFunc
|
||||
settingKey string
|
||||
expectVal string
|
||||
body string
|
||||
}{
|
||||
// ACL
|
||||
{"EnableACL", "POST", "/api/v1/security/acl/enable", h.EnableACL, "security.acl.enabled", "true", ""},
|
||||
{"DisableACL", "POST", "/api/v1/security/acl/disable", h.DisableACL, "security.acl.enabled", "false", ""},
|
||||
// ACL Patch
|
||||
{"PatchACL_True", "PATCH", "/api/v1/security/acl", h.PatchACL, "security.acl.enabled", "true", `{"enabled": true}`},
|
||||
{"PatchACL_False", "PATCH", "/api/v1/security/acl", h.PatchACL, "security.acl.enabled", "false", `{"enabled": false}`},
|
||||
|
||||
// WAF
|
||||
{"EnableWAF", "POST", "/api/v1/security/waf/enable", h.EnableWAF, "security.waf.enabled", "true", ""},
|
||||
{"DisableWAF", "POST", "/api/v1/security/waf/disable", h.DisableWAF, "security.waf.enabled", "false", ""},
|
||||
// WAF Patch
|
||||
{"PatchWAF_True", "PATCH", "/api/v1/security/waf", h.PatchWAF, "security.waf.enabled", "true", `{"enabled": true}`},
|
||||
{"PatchWAF_False", "PATCH", "/api/v1/security/waf", h.PatchWAF, "security.waf.enabled", "false", `{"enabled": false}`},
|
||||
|
||||
// Cerberus
|
||||
{"EnableCerberus", "POST", "/api/v1/security/cerberus/enable", h.EnableCerberus, "feature.cerberus.enabled", "true", ""},
|
||||
{"DisableCerberus", "POST", "/api/v1/security/cerberus/disable", h.DisableCerberus, "feature.cerberus.enabled", "false", ""},
|
||||
|
||||
// CrowdSec
|
||||
{"EnableCrowdSec", "POST", "/api/v1/security/crowdsec/enable", h.EnableCrowdSec, "security.crowdsec.enabled", "true", ""},
|
||||
{"DisableCrowdSec", "POST", "/api/v1/security/crowdsec/disable", h.DisableCrowdSec, "security.crowdsec.enabled", "false", ""},
|
||||
// CrowdSec Patch
|
||||
{"PatchCrowdSec_True", "PATCH", "/api/v1/security/crowdsec", h.PatchCrowdSec, "security.crowdsec.enabled", "true", `{"enabled": true}`},
|
||||
{"PatchCrowdSec_False", "PATCH", "/api/v1/security/crowdsec", h.PatchCrowdSec, "security.crowdsec.enabled", "false", `{"enabled": false}`},
|
||||
|
||||
// RateLimit
|
||||
{"EnableRateLimit", "POST", "/api/v1/security/rate-limit/enable", h.EnableRateLimit, "security.rate_limit.enabled", "true", ""},
|
||||
{"DisableRateLimit", "POST", "/api/v1/security/rate-limit/disable", h.DisableRateLimit, "security.rate_limit.enabled", "false", ""},
|
||||
// RateLimit Patch
|
||||
{"PatchRateLimit_True", "PATCH", "/api/v1/security/rate-limit", h.PatchRateLimit, "security.rate_limit.enabled", "true", `{"enabled": true}`},
|
||||
{"PatchRateLimit_False", "PATCH", "/api/v1/security/rate-limit", h.PatchRateLimit, "security.rate_limit.enabled", "false", `{"enabled": false}`},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
var req *http.Request
|
||||
if tc.body != "" {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, nil)
|
||||
}
|
||||
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
// Mock Admin Role
|
||||
c.Set("role", "admin")
|
||||
|
||||
tc.handler(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Verify Setting
|
||||
var setting models.Setting
|
||||
err := db.Where("key = ?", tc.settingKey).First(&setting).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expectVal, setting.Value)
|
||||
|
||||
if tc.expectVal == "true" && tc.settingKey != "feature.cerberus.enabled" {
|
||||
var cerberusSetting models.Setting
|
||||
err = db.Where("key = ?", "feature.cerberus.enabled").First(&cerberusSetting).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "true", cerberusSetting.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityToggles_Forbidden(t *testing.T) {
|
||||
h, _ := setupToggleTest(t)
|
||||
|
||||
// Just test one endpoint to verify role check
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil)
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
// No role set
|
||||
|
||||
h.EnableACL(c)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestPatchACL_InvalidBody(t *testing.T) {
|
||||
h, _ := setupToggleTest(t)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", "/api/v1/security/acl", strings.NewReader("invalid"))
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.PatchACL(c)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestPatchWAF_InvalidBody(t *testing.T) {
|
||||
h, _ := setupToggleTest(t)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", "/api/v1/security/waf", strings.NewReader("invalid"))
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.PatchWAF(c)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestPatchRateLimit_InvalidBody(t *testing.T) {
|
||||
h, _ := setupToggleTest(t)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", "/api/v1/security/rate-limit", strings.NewReader("invalid"))
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.PatchRateLimit(c)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestPatchCrowdSec_InvalidBody(t *testing.T) {
|
||||
h, _ := setupToggleTest(t)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", "/api/v1/security/crowdsec", strings.NewReader("invalid"))
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.PatchCrowdSec(c)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestACLForbiddenIfIPNotWhitelisted(t *testing.T) {
|
||||
h, db := setupToggleTest(t)
|
||||
|
||||
// Update config to have whitelist
|
||||
err := db.Model(&models.SecurityConfig{}).Where("name = ?", "default").Update("admin_whitelist", "10.0.0.1").Error
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil)
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
c.Request.RemoteAddr = "192.168.1.5:1234" // Different IP
|
||||
|
||||
h.EnableACL(c)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestACLEnabledIfIPWhitelisted(t *testing.T) {
|
||||
h, db := setupToggleTest(t)
|
||||
|
||||
// Update config to have whitelist
|
||||
err := db.Model(&models.SecurityConfig{}).Where("name = ?", "default").Update("admin_whitelist", "1.2.3.4").Error
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/security/acl/enable", nil)
|
||||
req.Header.Set("X-Forwarded-For", "1.2.3.4") // Trusted proxy simulation needed or direct RemoteAddr
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Request.RemoteAddr = "1.2.3.4:1234"
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.EnableACL(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestSecurityToggles_RollbackSettingWhenApplyFails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{}))
|
||||
require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error)
|
||||
require.NoError(t, db.Create(&models.Setting{Key: "security.waf.enabled", Value: "false", Category: "security", Type: "bool"}).Error)
|
||||
|
||||
manager := caddy.NewManager(
|
||||
caddy.NewClient("http://127.0.0.1:65535"),
|
||||
db,
|
||||
t.TempDir(),
|
||||
t.TempDir(),
|
||||
false,
|
||||
config.SecurityConfig{},
|
||||
)
|
||||
h := NewSecurityHandler(config.SecurityConfig{}, db, manager)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", "/api/v1/security/waf", strings.NewReader(`{"enabled":true}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
c.Set("role", "admin")
|
||||
|
||||
h.PatchWAF(c)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
var setting models.Setting
|
||||
require.NoError(t, db.Where("key = ?", "security.waf.enabled").First(&setting).Error)
|
||||
assert.Equal(t, "false", setting.Value)
|
||||
}
|
||||
Reference in New Issue
Block a user