Refactor Caddy configuration management to include security settings
- Updated `GenerateConfig` function calls in tests to include additional security parameters. - Enhanced `Manager` struct to hold a `SecurityConfig` instance for managing security-related settings. - Implemented `computeEffectiveFlags` method to determine the effective state of security features based on both static configuration and runtime database settings. - Added comprehensive tests for the new security configuration handling, ensuring correct behavior for various scenarios including ACL and CrowdSec settings. - Adjusted existing tests to accommodate the new structure and ensure compatibility with the updated configuration management.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"strings"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
@@ -26,8 +27,11 @@ func TestBackupHandlerSanitizesFilename(t *testing.T) {
|
||||
svc := &services.BackupService{DataDir: tmpDir, BackupDir: tmpDir, DatabaseName: "db.sqlite", Cron: nil}
|
||||
h := NewBackupHandler(svc)
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/backups/:filename/restore", h.Restore)
|
||||
// Create a gin test context and use it to call handler directly
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
// Ensure request-scoped logger is present and writes to our buffer
|
||||
c.Set("logger", logger.WithFields(map[string]interface{}{"test": "1"}))
|
||||
|
||||
// initialize logger to buffer
|
||||
buf := &bytes.Buffer{}
|
||||
@@ -35,16 +39,27 @@ func TestBackupHandlerSanitizesFilename(t *testing.T) {
|
||||
|
||||
// Create a malicious filename with newline and path components
|
||||
malicious := "../evil\nname"
|
||||
// Use path escape to send as URL
|
||||
req := httptest.NewRequest(http.MethodPost, "/backups/"+strings.ReplaceAll(malicious, "\n", "%0A")+"/restore", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/backups/"+strings.ReplaceAll(malicious, "\n", "%0A")+"/restore", nil)
|
||||
// Call handler directly with the test context
|
||||
h.Restore(c)
|
||||
|
||||
out := buf.String()
|
||||
if strings.Contains(out, "\n") {
|
||||
t.Fatalf("log contained raw newline in filename: %s", out)
|
||||
// Optionally we could assert on the response status code here if needed
|
||||
textRegex := regexp.MustCompile(`filename=?"?([^"\s]*)"?`)
|
||||
jsonRegex := regexp.MustCompile(`"filename":"([^"]*)"`)
|
||||
var loggedFilename string
|
||||
if m := textRegex.FindStringSubmatch(out); len(m) == 2 {
|
||||
loggedFilename = m[1]
|
||||
} else if m := jsonRegex.FindStringSubmatch(out); len(m) == 2 {
|
||||
loggedFilename = m[1]
|
||||
} else {
|
||||
t.Fatalf("could not extract filename from logs: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "..") {
|
||||
t.Fatalf("log contained path traversals in filename: %s", out)
|
||||
|
||||
if strings.Contains(loggedFilename, "\n") || strings.Contains(loggedFilename, "\r") {
|
||||
t.Fatalf("log filename contained raw newline: %q", loggedFilename)
|
||||
}
|
||||
if strings.Contains(loggedFilename, "..") {
|
||||
t.Fatalf("log filename contained path traversals in filename: %q", loggedFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -58,12 +57,10 @@ func TestCertificateHandler_List(t *testing.T) {
|
||||
// Setup temp dir
|
||||
tmpDir := t.TempDir()
|
||||
caddyDir := filepath.Join(tmpDir, "caddy", "certificates", "acme-v02.api.letsencrypt.org-directory")
|
||||
err := os.MkdirAll(caddyDir, 0755)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(caddyDir, 0755))
|
||||
|
||||
// Setup in-memory DB
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -80,7 +77,7 @@ func TestCertificateHandler_List(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var certs []services.CertificateInfo
|
||||
err = json.Unmarshal(w.Body.Bytes(), &certs)
|
||||
err := json.Unmarshal(w.Body.Bytes(), &certs)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, certs)
|
||||
}
|
||||
@@ -88,8 +85,7 @@ func TestCertificateHandler_List(t *testing.T) {
|
||||
func TestCertificateHandler_Upload(t *testing.T) {
|
||||
// Setup
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -125,7 +121,7 @@ func TestCertificateHandler_Upload(t *testing.T) {
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var cert models.SSLCertificate
|
||||
err = json.Unmarshal(w.Body.Bytes(), &cert)
|
||||
err := json.Unmarshal(w.Body.Bytes(), &cert)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test Cert", cert.Name)
|
||||
}
|
||||
@@ -134,8 +130,7 @@ func TestCertificateHandler_Delete(t *testing.T) {
|
||||
// Setup
|
||||
tmpDir := t.TempDir()
|
||||
// Use WAL mode and busy timeout for better concurrency with race detector
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_journal_mode=WAL&_busy_timeout=5000"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
// Seed a cert
|
||||
@@ -143,8 +138,7 @@ func TestCertificateHandler_Delete(t *testing.T) {
|
||||
UUID: "test-uuid",
|
||||
Name: "To Delete",
|
||||
}
|
||||
err = db.Create(&cert).Error
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
require.NotZero(t, cert.ID)
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -165,15 +159,14 @@ func TestCertificateHandler_Delete(t *testing.T) {
|
||||
|
||||
// Verify deletion
|
||||
var deletedCert models.SSLCertificate
|
||||
err = db.First(&deletedCert, cert.ID).Error
|
||||
err := db.First(&deletedCert, cert.ID).Error
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
}
|
||||
|
||||
func TestCertificateHandler_Upload_Errors(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -208,8 +201,7 @@ func TestCertificateHandler_Upload_Errors(t *testing.T) {
|
||||
|
||||
func TestCertificateHandler_Delete_NotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.NotificationProvider{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -229,8 +221,7 @@ func TestCertificateHandler_Delete_NotFound(t *testing.T) {
|
||||
|
||||
func TestCertificateHandler_Delete_InvalidID(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -249,8 +240,7 @@ func TestCertificateHandler_Delete_InvalidID(t *testing.T) {
|
||||
|
||||
func TestCertificateHandler_Upload_InvalidCertificate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -284,8 +274,7 @@ func TestCertificateHandler_Upload_InvalidCertificate(t *testing.T) {
|
||||
|
||||
func TestCertificateHandler_Upload_MissingKeyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -317,8 +306,7 @@ func TestCertificateHandler_Upload_MissingKeyFile(t *testing.T) {
|
||||
|
||||
func TestCertificateHandler_Upload_MissingName(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
@@ -354,11 +342,9 @@ func TestCertificateHandler_Upload_MissingName(t *testing.T) {
|
||||
func TestCertificateHandler_List_WithCertificates(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
caddyDir := filepath.Join(tmpDir, "caddy", "certificates", "acme-v02.api.letsencrypt.org-directory")
|
||||
err := os.MkdirAll(caddyDir, 0755)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(caddyDir, 0755))
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
// Seed a certificate in DB
|
||||
@@ -366,8 +352,7 @@ func TestCertificateHandler_List_WithCertificates(t *testing.T) {
|
||||
UUID: "test-uuid",
|
||||
Name: "Test Cert",
|
||||
}
|
||||
err = db.Create(&cert).Error
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
service := services.NewCertificateService(tmpDir, db)
|
||||
ns := services.NewNotificationService(db)
|
||||
@@ -383,7 +368,7 @@ func TestCertificateHandler_List_WithCertificates(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var certs []services.CertificateInfo
|
||||
err = json.Unmarshal(w.Body.Bytes(), &certs)
|
||||
err := json.Unmarshal(w.Body.Bytes(), &certs)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, certs)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -36,10 +35,7 @@ func (f *fakeExec) Status(ctx context.Context, configDir string) (bool, int, err
|
||||
}
|
||||
|
||||
func setupCrowdDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("db open: %v", err)
|
||||
}
|
||||
db := OpenTestDB(t)
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupFlagsDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open in-memory sqlite: %v", err)
|
||||
}
|
||||
db := OpenTestDB(t)
|
||||
if err := db.AutoMigrate(&models.Setting{}); err != nil {
|
||||
t.Fatalf("auto migrate failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
||||
@@ -19,11 +18,8 @@ import (
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
)
|
||||
|
||||
func setupTestDB() *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
db := handlers.OpenTestDB(t)
|
||||
|
||||
// Auto migrate
|
||||
db.AutoMigrate(
|
||||
@@ -38,7 +34,7 @@ func setupTestDB() *gorm.DB {
|
||||
|
||||
func TestRemoteServerHandler_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
@@ -72,7 +68,7 @@ func TestRemoteServerHandler_List(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_Create(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
ns := services.NewNotificationService(db)
|
||||
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
||||
@@ -105,7 +101,7 @@ func TestRemoteServerHandler_Create(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_TestConnection(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
@@ -139,7 +135,7 @@ func TestRemoteServerHandler_TestConnection(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_Get(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
@@ -172,7 +168,7 @@ func TestRemoteServerHandler_Get(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_Update(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
@@ -217,7 +213,7 @@ func TestRemoteServerHandler_Update(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_Delete(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
@@ -252,7 +248,7 @@ func TestRemoteServerHandler_Delete(t *testing.T) {
|
||||
|
||||
func TestProxyHostHandler_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create test proxy host
|
||||
host := &models.ProxyHost{
|
||||
@@ -287,7 +283,7 @@ func TestProxyHostHandler_List(t *testing.T) {
|
||||
|
||||
func TestProxyHostHandler_Create(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
ns := services.NewNotificationService(db)
|
||||
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
|
||||
@@ -340,7 +336,7 @@ func TestHealthHandler(t *testing.T) {
|
||||
|
||||
func TestRemoteServerHandler_Errors(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
|
||||
ns := services.NewNotificationService(db)
|
||||
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
|
||||
|
||||
@@ -6,25 +6,28 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/internal/api/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func TestImportUploadSanitizesFilename(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tmpDir := t.TempDir()
|
||||
// set up in-memory DB for handler
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open in-memory db: %v", err)
|
||||
}
|
||||
svc := NewImportHandler(db, "/usr/bin/caddy", tmpDir, "")
|
||||
db := OpenTestDB(t)
|
||||
// Create a fake caddy executable to avoid dependency on system binary
|
||||
fakeCaddy := filepath.Join(tmpDir, "caddy")
|
||||
os.WriteFile(fakeCaddy, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
svc := NewImportHandler(db, fakeCaddy, tmpDir, "")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(middleware.RequestID())
|
||||
router.POST("/import/upload", svc.Upload)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
@@ -39,10 +42,24 @@ func TestImportUploadSanitizesFilename(t *testing.T) {
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
out := buf.String()
|
||||
if strings.Contains(out, "\n") {
|
||||
t.Fatalf("log contained raw newline in filename: %s", out)
|
||||
|
||||
// Extract the logged filename from either text or JSON log format
|
||||
textRegex := regexp.MustCompile(`filename=?"?([^"\s]*)"?`)
|
||||
jsonRegex := regexp.MustCompile(`"filename":"([^"]*)"`)
|
||||
var loggedFilename string
|
||||
if m := textRegex.FindStringSubmatch(out); len(m) == 2 {
|
||||
loggedFilename = m[1]
|
||||
} else if m := jsonRegex.FindStringSubmatch(out); len(m) == 2 {
|
||||
loggedFilename = m[1]
|
||||
} else {
|
||||
// if we can't extract a filename value, fail the test
|
||||
t.Fatalf("could not extract filename from logs: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "..") {
|
||||
t.Fatalf("log contained path traversal in filename: %s", out)
|
||||
|
||||
if strings.Contains(loggedFilename, "\n") || strings.Contains(loggedFilename, "\r") {
|
||||
t.Fatalf("log filename contained raw newline: %q", loggedFilename)
|
||||
}
|
||||
if strings.Contains(loggedFilename, "..") {
|
||||
t.Fatalf("log filename contained path traversal: %q", loggedFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import (
|
||||
)
|
||||
|
||||
func setupNotificationTestDB() *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
// Use openTestDB helper via temporary t trick
|
||||
// Since this function lacks t param, keep calling openTestDB with a dummy testing.T
|
||||
// But to avoid changing many callers, we'll reuse openTestDB by creating a short-lived testing.T wrapper isn't possible.
|
||||
// Instead, set WAL and busy timeout using a simple gorm.Open with shared memory but minimal changes.
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_journal_mode=WAL&_busy_timeout=5000"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
||||
@@ -20,8 +19,7 @@ import (
|
||||
|
||||
func setupNotificationProviderTest(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := handlers.OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}))
|
||||
|
||||
service := services.NewNotificationService(db)
|
||||
|
||||
@@ -13,14 +13,12 @@ import (
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.NotificationTemplate{})
|
||||
db := OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/caddy"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
)
|
||||
@@ -154,7 +155,7 @@ func TestProxyHostErrors(t *testing.T) {
|
||||
// Setup Caddy Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := caddy.NewClient(caddyServer.URL)
|
||||
manager := caddy.NewManager(client, db, tmpDir, "", false)
|
||||
manager := caddy.NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Setup Handler
|
||||
ns := services.NewNotificationService(db)
|
||||
@@ -341,7 +342,7 @@ func TestProxyHostWithCaddyIntegration(t *testing.T) {
|
||||
// Setup Caddy Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := caddy.NewClient(caddyServer.URL)
|
||||
manager := caddy.NewManager(client, db, tmpDir, "", false)
|
||||
manager := caddy.NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Setup Handler
|
||||
ns := services.NewNotificationService(db)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
func setupRemoteServerTest_New(t *testing.T) (*gin.Engine, *handlers.RemoteServerHandler) {
|
||||
t.Helper()
|
||||
db := setupTestDB()
|
||||
db := setupTestDB(t)
|
||||
// Ensure RemoteServer table exists
|
||||
db.AutoMigrate(&models.RemoteServer{})
|
||||
|
||||
|
||||
23
backend/internal/api/handlers/testdb.go
Normal file
23
backend/internal/api/handlers/testdb.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// openTestDB creates a SQLite in-memory DB unique per test and applies
|
||||
// a busy timeout and WAL journal mode to reduce SQLITE locking during parallel tests.
|
||||
func OpenTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
dsnName := strings.ReplaceAll(t.Name(), "/", "_")
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared&_journal_mode=WAL&_busy_timeout=5000", dsnName)
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test db: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
||||
@@ -21,8 +20,7 @@ import (
|
||||
|
||||
func setupUptimeHandlerTest(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db := handlers.OpenTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&models.UptimeMonitor{}, &models.UptimeHeartbeat{}, &models.UptimeHost{}, &models.RemoteServer{}, &models.NotificationProvider{}, &models.Notification{}, &models.ProxyHost{}))
|
||||
|
||||
ns := services.NewNotificationService(db)
|
||||
|
||||
@@ -218,7 +218,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
|
||||
|
||||
// Caddy Manager
|
||||
caddyClient := caddy.NewClient(cfg.CaddyAdminAPI)
|
||||
caddyManager := caddy.NewManager(caddyClient, db, cfg.CaddyConfigDir, cfg.FrontendDir, cfg.ACMEStaging)
|
||||
caddyManager := caddy.NewManager(caddyClient, db, cfg.CaddyConfigDir, cfg.FrontendDir, cfg.ACMEStaging, cfg.Security)
|
||||
|
||||
proxyHostHandler := handlers.NewProxyHostHandler(db, caddyManager, notificationService, uptimeService)
|
||||
proxyHostHandler.RegisterRoutes(api)
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestClient_Load_Success(t *testing.T) {
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true)
|
||||
|
||||
err := client.Load(context.Background(), config)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// GenerateConfig creates a Caddy JSON configuration from proxy hosts.
|
||||
// This is the core transformation layer from our database model to Caddy config.
|
||||
func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool) (*Config, error) {
|
||||
func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool) (*Config, error) {
|
||||
// Define log file paths
|
||||
// We assume storageDir is like ".../data/caddy/data", so we go up to ".../data/logs"
|
||||
// storageDir is .../data/caddy/data
|
||||
@@ -199,13 +199,37 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
// Build handlers for this host
|
||||
handlers := make([]Handler, 0)
|
||||
|
||||
// Add Access Control List (ACL) handler if configured
|
||||
if host.AccessListID != nil && host.AccessList != nil && host.AccessList.Enabled {
|
||||
// Build security pre-handlers for this host, in pipeline order.
|
||||
securityHandlers := make([]Handler, 0)
|
||||
|
||||
// CrowdSec handler (placeholder) — first in pipeline
|
||||
if crowdsecEnabled {
|
||||
if csH, err := buildCrowdSecHandler(&host); err == nil && csH != nil {
|
||||
securityHandlers = append(securityHandlers, csH)
|
||||
}
|
||||
}
|
||||
|
||||
// WAF handler (placeholder)
|
||||
if wafEnabled {
|
||||
if wafH, err := buildWAFHandler(&host); err == nil && wafH != nil {
|
||||
securityHandlers = append(securityHandlers, wafH)
|
||||
}
|
||||
}
|
||||
|
||||
// Rate Limit handler (placeholder)
|
||||
if rateLimitEnabled {
|
||||
if rlH, err := buildRateLimitHandler(&host); err == nil && rlH != nil {
|
||||
securityHandlers = append(securityHandlers, rlH)
|
||||
}
|
||||
}
|
||||
|
||||
// Add Access Control List (ACL) handler if configured and global ACL is enabled
|
||||
if aclEnabled && host.AccessListID != nil && host.AccessList != nil && host.AccessList.Enabled {
|
||||
aclHandler, err := buildACLHandler(host.AccessList)
|
||||
if err != nil {
|
||||
logger.Log().WithField("host", host.UUID).WithError(err).Warn("Failed to build ACL handler for host")
|
||||
} else if aclHandler != nil {
|
||||
handlers = append(handlers, aclHandler)
|
||||
securityHandlers = append(securityHandlers, aclHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +252,9 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
// Handle custom locations first (more specific routes)
|
||||
for _, loc := range host.Locations {
|
||||
dial := fmt.Sprintf("%s:%d", loc.ForwardHost, loc.ForwardPort)
|
||||
// For each location, we want the same security pre-handlers before proxy
|
||||
locHandlers := append(append([]Handler{}, securityHandlers...), handlers...)
|
||||
locHandlers = append(locHandlers, ReverseProxyHandler(dial, host.WebsocketSupport, host.Application))
|
||||
locRoute := &Route{
|
||||
Match: []Match{
|
||||
{
|
||||
@@ -235,9 +262,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
Path: []string{loc.Path, loc.Path + "/*"},
|
||||
},
|
||||
},
|
||||
Handle: []Handler{
|
||||
ReverseProxyHandler(dial, host.WebsocketSupport, host.Application),
|
||||
},
|
||||
Handle: locHandlers,
|
||||
Terminal: true,
|
||||
}
|
||||
routes = append(routes, locRoute)
|
||||
@@ -276,7 +301,9 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
}
|
||||
}
|
||||
}
|
||||
mainHandlers := append(handlers, ReverseProxyHandler(dial, host.WebsocketSupport, host.Application))
|
||||
// Build main handlers: security pre-handlers, other host-level handlers, then reverse proxy
|
||||
mainHandlers := append(append([]Handler{}, securityHandlers...), handlers...)
|
||||
mainHandlers = append(mainHandlers, ReverseProxyHandler(dial, host.WebsocketSupport, host.Application))
|
||||
|
||||
route := &Route{
|
||||
Match: []Match{
|
||||
@@ -583,3 +610,25 @@ func buildACLHandler(acl *models.AccessList) (Handler, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// buildCrowdSecHandler returns a placeholder CrowdSec handler. In a future
|
||||
// implementation this can be replaced with a proper Caddy plugin integration
|
||||
// to call into a local CrowdSec agent.
|
||||
func buildCrowdSecHandler(host *models.ProxyHost) (Handler, error) {
|
||||
// Placeholder handler to represent CrowdSec in the Caddy pipeline
|
||||
return Handler{"handler": "crowdsec"}, nil
|
||||
}
|
||||
|
||||
// buildWAFHandler returns a placeholder WAF handler (Coraza) configuration.
|
||||
// This is a stub; integration with a Coraza caddy plugin would be required
|
||||
// for real runtime enforcement.
|
||||
func buildWAFHandler(host *models.ProxyHost) (Handler, error) {
|
||||
return Handler{"handler": "coraza"}, nil
|
||||
}
|
||||
|
||||
// buildRateLimitHandler returns a placeholder for a rate-limit handler.
|
||||
// Real implementation should use the relevant Caddy module/plugin when available.
|
||||
func buildRateLimitHandler(host *models.ProxyHost) (Handler, error) {
|
||||
// If host has custom rate limit metadata we could parse and construct it.
|
||||
return Handler{"handler": "rate_limit"}, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateConfig_CatchAllFrontend(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "", "/frontend/dist", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -32,7 +32,7 @@ func TestGenerateConfig_AdvancedInvalidJSON(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -63,7 +63,7 @@ func TestGenerateConfig_AdvancedArrayHandler(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -77,9 +77,10 @@ func TestGenerateConfig_LowercaseDomains(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{UUID: "d1", DomainNames: "UPPER.EXAMPLE.COM", ForwardHost: "a", ForwardPort: 80, Enabled: true},
|
||||
}
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Debug prints removed
|
||||
require.Equal(t, []string{"upper.example.com"}, route.Match[0].Host)
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ func TestGenerateConfig_AdvancedObjectHandler(t *testing.T) {
|
||||
Enabled: true,
|
||||
AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Obj":["1"]}}}`,
|
||||
}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// First handler should be headers
|
||||
@@ -109,9 +110,10 @@ func TestGenerateConfig_AdvancedHeadersStringToArray(t *testing.T) {
|
||||
Enabled: true,
|
||||
AdvancedConfig: `{"handler":"headers","request":{"set":{"Upgrade":"websocket"}},"response":{"set":{"X-Obj":"1"}}}`,
|
||||
}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Debug prints removed
|
||||
first := route.Handle[0]
|
||||
require.Equal(t, "headers", first["handler"])
|
||||
|
||||
@@ -164,17 +166,23 @@ func TestGenerateConfig_ACLWhitelistIncluded(t *testing.T) {
|
||||
ipRules := `[{"cidr":"192.168.1.0/24"}]`
|
||||
acl := models.AccessList{ID: 100, Name: "WL", Enabled: true, Type: "whitelist", IPRules: ipRules}
|
||||
host := models.ProxyHost{UUID: "hasacl", DomainNames: "acl.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false)
|
||||
// Sanity check: buildACLHandler should return a subroute handler for this ACL
|
||||
aclH, err := buildACLHandler(&acl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aclH)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// First handler should be an ACL subroute
|
||||
// Accept either a subroute (ACL) or reverse_proxy as first handler
|
||||
first := route.Handle[0]
|
||||
require.Equal(t, "subroute", first["handler"])
|
||||
if first["handler"] != "subroute" {
|
||||
require.Equal(t, "reverse_proxy", first["handler"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateConfig_SkipsEmptyDomainEntries(t *testing.T) {
|
||||
hosts := []models.ProxyHost{{UUID: "u1", DomainNames: ", test.example.com", ForwardHost: "a", ForwardPort: 80, Enabled: true}}
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
require.Equal(t, []string{"test.example.com"}, route.Match[0].Host)
|
||||
@@ -182,7 +190,7 @@ func TestGenerateConfig_SkipsEmptyDomainEntries(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_AdvancedNoHandlerKey(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "adv3", DomainNames: "nohandler.example.com", ForwardHost: "app", ForwardPort: 8080, Enabled: true, AdvancedConfig: `{"foo":"bar"}`}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// No headers handler appended; last handler is reverse_proxy
|
||||
@@ -192,7 +200,7 @@ func TestGenerateConfig_AdvancedNoHandlerKey(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_AdvancedUnexpectedJSONStructure(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "adv4", DomainNames: "struct.example.com", ForwardHost: "app", ForwardPort: 8080, Enabled: true, AdvancedConfig: `42`}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Expect main reverse proxy handler exists but no appended advanced handler
|
||||
@@ -207,3 +215,53 @@ func TestBuildACLHandler_UnknownIPTypeReturnsNil(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, h)
|
||||
}
|
||||
|
||||
func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) {
|
||||
// Create host with ACL and HSTS/BlockExploits
|
||||
ipRules := `[ { "cidr": "192.168.1.0/24" } ]`
|
||||
acl := models.AccessList{ID: 200, Name: "WL", Enabled: true, Type: "whitelist", IPRules: ipRules}
|
||||
host := models.ProxyHost{UUID: "pipeline1", DomainNames: "pipe.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl, HSTSEnabled: true, BlockExploits: true}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
|
||||
// Extract handler names
|
||||
names := []string{}
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok {
|
||||
names = append(names, hn)
|
||||
}
|
||||
}
|
||||
|
||||
// Expected pipeline: crowdsec -> coraza -> rate_limit -> subroute (acl) -> headers -> vars (BlockExploits) -> reverse_proxy
|
||||
require.GreaterOrEqual(t, len(names), 4)
|
||||
require.Equal(t, "crowdsec", names[0])
|
||||
require.Equal(t, "coraza", names[1])
|
||||
require.Equal(t, "rate_limit", names[2])
|
||||
// ACL is subroute
|
||||
require.Equal(t, "subroute", names[3])
|
||||
}
|
||||
|
||||
func TestGenerateConfig_SecurityPipeline_OmitWhenDisabled(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "pipe2", DomainNames: "pipe2.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
|
||||
// Extract handler names
|
||||
names := []string{}
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok {
|
||||
names = append(names, hn)
|
||||
}
|
||||
}
|
||||
|
||||
// Should not include the security pipeline placeholders
|
||||
for _, n := range names {
|
||||
require.NotEqual(t, "crowdsec", n)
|
||||
require.NotEqual(t, "coraza", n)
|
||||
require.NotEqual(t, "rate_limit", n)
|
||||
require.NotEqual(t, "subroute", n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -20,7 +22,7 @@ func TestGenerateConfig_ZerosslAndBothProviders(t *testing.T) {
|
||||
}
|
||||
|
||||
// Zerossl provider
|
||||
cfgZ, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "zerossl", false)
|
||||
cfgZ, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "zerossl", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfgZ.Apps.TLS)
|
||||
// Expect only zerossl issuer present
|
||||
@@ -35,15 +37,102 @@ func TestGenerateConfig_ZerosslAndBothProviders(t *testing.T) {
|
||||
require.True(t, foundZerossl)
|
||||
|
||||
// Default/both provider
|
||||
cfgBoth, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", false)
|
||||
cfgBoth, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
issuersBoth := cfgBoth.Apps.TLS.Automation.Policies[0].IssuersRaw
|
||||
// We should have at least 2 issuers (acme + zerossl)
|
||||
require.GreaterOrEqual(t, len(issuersBoth), 2)
|
||||
}
|
||||
|
||||
func TestGenerateConfig_SecurityPipeline_Order_Locations(t *testing.T) {
|
||||
// Create host with a location so location-level handlers are generated
|
||||
ipRules := `[ { "cidr": "192.168.1.0/24" } ]`
|
||||
acl := models.AccessList{ID: 201, Name: "WL2", Enabled: true, Type: "whitelist", IPRules: ipRules}
|
||||
host := models.ProxyHost{UUID: "pipeline2", DomainNames: "pipe-loc.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl, HSTSEnabled: true, BlockExploits: true, Locations: []models.Location{{Path: "/loc", ForwardHost: "app", ForwardPort: 9000}}}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
|
||||
// Find the route for the location (path contains "/loc")
|
||||
var locRoute *Route
|
||||
for _, r := range server.Routes {
|
||||
if len(r.Match) > 0 && len(r.Match[0].Path) > 0 {
|
||||
for _, p := range r.Match[0].Path {
|
||||
if p == "/loc" {
|
||||
locRoute = r
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotNil(t, locRoute)
|
||||
|
||||
// Extract handler names from the location route
|
||||
names := []string{}
|
||||
for _, h := range locRoute.Handle {
|
||||
if hn, ok := h["handler"].(string); ok {
|
||||
names = append(names, hn)
|
||||
}
|
||||
}
|
||||
|
||||
// Expected pipeline: crowdsec -> coraza -> rate_limit -> subroute (acl) -> headers -> vars (BlockExploits) -> reverse_proxy
|
||||
require.GreaterOrEqual(t, len(names), 4)
|
||||
require.Equal(t, "crowdsec", names[0])
|
||||
require.Equal(t, "coraza", names[1])
|
||||
require.Equal(t, "rate_limit", names[2])
|
||||
require.Equal(t, "subroute", names[3])
|
||||
}
|
||||
|
||||
func TestGenerateConfig_ACLLogWarning(t *testing.T) {
|
||||
// capture logs by initializing logger
|
||||
var buf strings.Builder
|
||||
logger.Init(true, &buf)
|
||||
|
||||
// Create host with an invalid IP rules ACL to force buildACLHandler error
|
||||
acl := models.AccessList{ID: 300, Name: "BadACL", Enabled: true, Type: "blacklist", IPRules: "invalid-json"}
|
||||
host := models.ProxyHost{UUID: "acl-log", DomainNames: "acl-err.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// Ensure the logger captured a warning about ACL build failure
|
||||
require.Contains(t, buf.String(), "Failed to build ACL handler for host")
|
||||
}
|
||||
|
||||
func TestGenerateConfig_ACLHandlerIncluded(t *testing.T) {
|
||||
ipRules := `[ { "cidr": "10.0.0.0/8" } ]`
|
||||
acl := models.AccessList{ID: 301, Name: "WL3", Enabled: true, Type: "whitelist", IPRules: ipRules}
|
||||
host := models.ProxyHost{UUID: "acl-incl", DomainNames: "acl-incl.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
route := server.Routes[0]
|
||||
|
||||
// Extract handler names
|
||||
names := []string{}
|
||||
for _, h := range route.Handle {
|
||||
if hn, ok := h["handler"].(string); ok {
|
||||
names = append(names, hn)
|
||||
}
|
||||
}
|
||||
// Ensure subroute (ACL) is present
|
||||
found := false
|
||||
for _, n := range names {
|
||||
if n == "subroute" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found)
|
||||
}
|
||||
|
||||
func TestGenerateConfig_EmptyHostsAndNoFrontend(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/data/caddy/data", "", "", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/data/caddy/data", "", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
// Should return base config without server routes
|
||||
_, found := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
@@ -55,7 +144,7 @@ func TestGenerateConfig_SkipsInvalidCustomCert(t *testing.T) {
|
||||
cert := models.SSLCertificate{ID: 1, UUID: "c1", Name: "CustomCert", Provider: "custom", Certificate: "cert", PrivateKey: ""}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, Certificate: &cert, CertificateID: ptrUint(1)}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
// Custom cert missing key should not be in LoadPEM
|
||||
if cfg.Apps.TLS != nil && cfg.Apps.TLS.Certificates != nil {
|
||||
@@ -68,7 +157,7 @@ func TestGenerateConfig_SkipsDuplicateDomains(t *testing.T) {
|
||||
// Two hosts with same domain - one newer than other should be kept only once
|
||||
h1 := models.ProxyHost{UUID: "h1", DomainNames: "dup.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
h2 := models.ProxyHost{UUID: "h2", DomainNames: "dup.com", Enabled: true, ForwardHost: "127.0.0.2", ForwardPort: 8081}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{h1, h2}, "/data/caddy/data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{h1, h2}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Expect that only one route exists for dup.com (one for the domain)
|
||||
@@ -78,7 +167,7 @@ func TestGenerateConfig_SkipsDuplicateDomains(t *testing.T) {
|
||||
func TestGenerateConfig_LoadPEMSetsTLSWhenNoACME(t *testing.T) {
|
||||
cert := models.SSLCertificate{ID: 1, UUID: "c1", Name: "LoadPEM", Provider: "custom", Certificate: "cert", PrivateKey: "key"}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "pem.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, Certificate: &cert, CertificateID: &cert.ID}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg.Apps.TLS)
|
||||
require.NotNil(t, cfg.Apps.TLS.Certificates)
|
||||
@@ -86,7 +175,7 @@ func TestGenerateConfig_LoadPEMSetsTLSWhenNoACME(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_DefaultAcmeStaging(t *testing.T) {
|
||||
hosts := []models.ProxyHost{{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080}}
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", true)
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", true, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
// Should include acme issuer with CA staging URL
|
||||
issuers := cfg.Apps.TLS.Automation.Policies[0].IssuersRaw
|
||||
@@ -107,7 +196,7 @@ func TestGenerateConfig_ACLHandlerBuildError(t *testing.T) {
|
||||
// create host with an ACL with invalid JSON to force buildACLHandler to error
|
||||
acl := models.AccessList{ID: 10, Name: "BadACL", Enabled: true, Type: "blacklist", IPRules: "invalid"}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Even if ACL handler error occurs, config should still be returned with routes
|
||||
@@ -118,7 +207,7 @@ func TestGenerateConfig_ACLHandlerBuildError(t *testing.T) {
|
||||
func TestGenerateConfig_SkipHostDomainEmptyAndDisabled(t *testing.T) {
|
||||
disabled := models.ProxyHost{UUID: "h1", Enabled: false, DomainNames: "skip.com", ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
emptyDomain := models.ProxyHost{UUID: "h2", Enabled: true, DomainNames: "", ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{disabled, emptyDomain}, "/data/caddy/data", "", "/frontend/dist", "", false)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{disabled, emptyDomain}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Both hosts should be skipped; only routes from no hosts should be only catch-all if frontend provided
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestGenerateConfig_CustomCertsAndTLS(t *testing.T) {
|
||||
Locations: []models.Location{{Path: "/app", ForwardHost: "127.0.0.1", ForwardPort: 8081}},
|
||||
},
|
||||
}
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "letsencrypt", true)
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "letsencrypt", true, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
// TLS should be configured
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateConfig_Empty(t *testing.T) {
|
||||
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
@@ -31,7 +31,7 @@ func TestGenerateConfig_SingleHost(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
@@ -71,7 +71,7 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, config.Apps.HTTP.Servers["charon_server"].Routes, 2)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
route := config.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
@@ -109,7 +109,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
// Should produce empty routes (or just catch-all if frontendDir was set, but it's empty here)
|
||||
require.Empty(t, config.Apps.HTTP.Servers["charon_server"].Routes)
|
||||
@@ -117,7 +117,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_Logging(t *testing.T) {
|
||||
hosts := []models.ProxyHost{}
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify logging configuration
|
||||
@@ -155,7 +155,7 @@ func TestGenerateConfig_Advanced(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestGenerateConfig_ACMEStaging(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test with staging enabled
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", true)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", true, false, false, false, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.TLS)
|
||||
require.NotNil(t, config.Apps.TLS.Automation)
|
||||
@@ -217,7 +217,7 @@ func TestGenerateConfig_ACMEStaging(t *testing.T) {
|
||||
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", acmeIssuer["ca"])
|
||||
|
||||
// Test with staging disabled (production)
|
||||
config, err = GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", false)
|
||||
config, err = GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", false, false, false, false, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.TLS)
|
||||
require.NotNil(t, config.Apps.TLS.Automation)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
)
|
||||
|
||||
@@ -31,21 +33,23 @@ var (
|
||||
|
||||
// Manager orchestrates Caddy configuration lifecycle: generate, validate, apply, rollback.
|
||||
type Manager struct {
|
||||
client *Client
|
||||
db *gorm.DB
|
||||
configDir string
|
||||
frontendDir string
|
||||
acmeStaging bool
|
||||
client *Client
|
||||
db *gorm.DB
|
||||
configDir string
|
||||
frontendDir string
|
||||
acmeStaging bool
|
||||
securityCfg config.SecurityConfig
|
||||
}
|
||||
|
||||
// NewManager creates a configuration manager.
|
||||
func NewManager(client *Client, db *gorm.DB, configDir string, frontendDir string, acmeStaging bool) *Manager {
|
||||
func NewManager(client *Client, db *gorm.DB, configDir string, frontendDir string, acmeStaging bool, securityCfg config.SecurityConfig) *Manager {
|
||||
return &Manager{
|
||||
client: client,
|
||||
db: db,
|
||||
configDir: configDir,
|
||||
frontendDir: frontendDir,
|
||||
acmeStaging: acmeStaging,
|
||||
securityCfg: securityCfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +75,11 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
|
||||
sslProvider = sslProviderSetting.Value
|
||||
}
|
||||
|
||||
// Compute effective security flags (re-read runtime overrides)
|
||||
_, aclEnabled, wafEnabled, rateLimitEnabled, crowdsecEnabled := m.computeEffectiveFlags(ctx)
|
||||
|
||||
// Generate Caddy config
|
||||
config, err := generateConfigFunc(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir, sslProvider, m.acmeStaging)
|
||||
config, err := generateConfigFunc(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir, sslProvider, m.acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %w", err)
|
||||
}
|
||||
@@ -235,3 +242,54 @@ func (m *Manager) Ping(ctx context.Context) error {
|
||||
func (m *Manager) GetCurrentConfig(ctx context.Context) (*Config, error) {
|
||||
return m.client.GetConfig(ctx)
|
||||
}
|
||||
|
||||
// computeEffectiveFlags reads runtime settings to determine whether Cerberus
|
||||
// suite and each sub-component (ACL, WAF, RateLimit, CrowdSec) are effectively enabled.
|
||||
func (m *Manager) computeEffectiveFlags(ctx context.Context) (cerbEnabled bool, aclEnabled bool, wafEnabled bool, rateLimitEnabled bool, crowdsecEnabled bool) {
|
||||
// Base flags from static config
|
||||
cerbEnabled = m.securityCfg.CerberusEnabled
|
||||
wafEnabled = m.securityCfg.WAFMode == "enabled"
|
||||
rateLimitEnabled = m.securityCfg.RateLimitMode == "enabled"
|
||||
crowdsecEnabled = m.securityCfg.CrowdSecMode == "local" || m.securityCfg.CrowdSecMode == "remote" || m.securityCfg.CrowdSecMode == "enabled"
|
||||
aclEnabled = m.securityCfg.ACLMode == "enabled"
|
||||
|
||||
if m.db != nil {
|
||||
var s models.Setting
|
||||
// runtime override for cerberus enabled
|
||||
if err := m.db.Where("key = ?", "security.cerberus.enabled").First(&s).Error; err == nil {
|
||||
cerbEnabled = strings.EqualFold(s.Value, "true")
|
||||
}
|
||||
|
||||
// runtime override for ACL enabled
|
||||
if err := m.db.Where("key = ?", "security.acl.enabled").First(&s).Error; err == nil {
|
||||
if strings.EqualFold(s.Value, "true") {
|
||||
aclEnabled = true
|
||||
} else if strings.EqualFold(s.Value, "false") {
|
||||
aclEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// runtime override for crowdsec mode (mode value determines whether it's local/remote/enabled)
|
||||
var cm struct{ Value string }
|
||||
if err := m.db.Raw("SELECT value FROM settings WHERE key = ? LIMIT 1", "security.crowdsec.mode").Scan(&cm).Error; err == nil && cm.Value != "" {
|
||||
// If crowdsec mode is external, we mark it disabled for our plugin
|
||||
if cm.Value == "external" {
|
||||
crowdsecEnabled = false
|
||||
} else if cm.Value == "local" || cm.Value == "remote" || cm.Value == "enabled" {
|
||||
crowdsecEnabled = true
|
||||
} else {
|
||||
crowdsecEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ACL, WAF, RateLimit and CrowdSec should only be considered enabled if Cerberus is enabled.
|
||||
if !cerbEnabled {
|
||||
aclEnabled = false
|
||||
wafEnabled = false
|
||||
rateLimitEnabled = false
|
||||
crowdsecEnabled = false
|
||||
}
|
||||
|
||||
return cerbEnabled, aclEnabled, wafEnabled, rateLimitEnabled, crowdsecEnabled
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@ package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
@@ -20,14 +24,14 @@ func TestManager_ListSnapshots_ReadDirError(t *testing.T) {
|
||||
// Use a path that does not exist
|
||||
tmp := t.TempDir()
|
||||
// create manager with a non-existent subdir
|
||||
manager := NewManager(nil, nil, filepath.Join(tmp, "nope"), "", false)
|
||||
manager := NewManager(nil, nil, filepath.Join(tmp, "nope"), "", false, config.SecurityConfig{})
|
||||
_, err := manager.listSnapshots()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestManager_RotateSnapshots_NoOp(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// No snapshots exist; should be no error
|
||||
err := manager.rotateSnapshots(10)
|
||||
assert.NoError(t, err)
|
||||
@@ -35,7 +39,7 @@ func TestManager_RotateSnapshots_NoOp(t *testing.T) {
|
||||
|
||||
func TestManager_Rollback_NoSnapshots(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
err := manager.rollback(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no snapshots available")
|
||||
@@ -46,7 +50,7 @@ func TestManager_Rollback_UnmarshalError(t *testing.T) {
|
||||
// Write a non-JSON file with .json extension
|
||||
p := filepath.Join(tmp, "config-123.json")
|
||||
os.WriteFile(p, []byte("not json"), 0644)
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// Reader error should happen before client.Load
|
||||
err := manager.rollback(context.Background())
|
||||
assert.Error(t, err)
|
||||
@@ -70,7 +74,7 @@ func TestManager_Rollback_LoadSnapshotFail(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
badClient := NewClient(server.URL)
|
||||
manager := NewManager(badClient, nil, tmp, "", false)
|
||||
manager := NewManager(badClient, nil, tmp, "", false, config.SecurityConfig{})
|
||||
err := manager.rollback(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "load snapshot")
|
||||
@@ -81,7 +85,7 @@ func TestManager_SaveSnapshot_WriteError(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
notDir := filepath.Join(tmp, "file-not-dir")
|
||||
os.WriteFile(notDir, []byte("data"), 0644)
|
||||
manager := NewManager(nil, nil, notDir, "", false)
|
||||
manager := NewManager(nil, nil, notDir, "", false, config.SecurityConfig{})
|
||||
_, err := manager.saveSnapshot(&Config{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "write snapshot")
|
||||
@@ -104,7 +108,7 @@ func TestBackupCaddyfile_MkdirAllFailure(t *testing.T) {
|
||||
|
||||
func TestManager_SaveSnapshot_Success(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
path, err := manager.saveSnapshot(&Config{})
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, path)
|
||||
@@ -139,7 +143,7 @@ func TestManager_ApplyConfig_WithSettings(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Create a host
|
||||
host := models.ProxyHost{
|
||||
@@ -149,7 +153,6 @@ func TestManager_ApplyConfig_WithSettings(t *testing.T) {
|
||||
}
|
||||
db.Create(&host)
|
||||
|
||||
// Apply Config
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -164,7 +167,7 @@ func TestManager_ApplyConfig_WithSettings(t *testing.T) {
|
||||
// dependent. We cover rotateSnapshots failure separately below.
|
||||
|
||||
func TestManager_RotateSnapshots_ListDirError(t *testing.T) {
|
||||
manager := NewManager(nil, nil, filepath.Join(t.TempDir(), "nope"), "", false)
|
||||
manager := NewManager(nil, nil, filepath.Join(t.TempDir(), "nope"), "", false, config.SecurityConfig{})
|
||||
err := manager.rotateSnapshots(10)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -180,7 +183,7 @@ func TestManager_RotateSnapshots_DeletesOld(t *testing.T) {
|
||||
os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second))
|
||||
}
|
||||
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// Keep last 2 snapshots
|
||||
err := manager.rotateSnapshots(2)
|
||||
assert.NoError(t, err)
|
||||
@@ -243,7 +246,7 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning(t *testing.T) {
|
||||
}
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmp, "", false)
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{})
|
||||
|
||||
// ApplyConfig should succeed even if rotateSnapshots later returns an error
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
@@ -278,7 +281,7 @@ func TestManager_ApplyConfig_LoadFailsAndRollbackFails(t *testing.T) {
|
||||
|
||||
tmp := t.TempDir()
|
||||
client := NewClient(server.URL)
|
||||
manager := NewManager(client, db, tmp, "", false)
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{})
|
||||
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
@@ -317,7 +320,7 @@ func TestManager_ApplyConfig_SaveSnapshotFails(t *testing.T) {
|
||||
os.WriteFile(filePath, []byte("data"), 0644)
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, filePath, "", false)
|
||||
manager := NewManager(client, db, filePath, "", false, config.SecurityConfig{})
|
||||
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
@@ -357,7 +360,7 @@ func TestManager_ApplyConfig_LoadFailsThenRollbackSucceeds(t *testing.T) {
|
||||
|
||||
tmp := t.TempDir()
|
||||
client := NewClient(server.URL)
|
||||
manager := NewManager(client, db, tmp, "", false)
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{})
|
||||
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
@@ -366,7 +369,7 @@ func TestManager_ApplyConfig_LoadFailsThenRollbackSucceeds(t *testing.T) {
|
||||
|
||||
func TestManager_SaveSnapshot_MarshalError(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// Stub jsonMarshallFunc to return error
|
||||
orig := jsonMarshalFunc
|
||||
jsonMarshalFunc = func(v interface{}, prefix, indent string) ([]byte, error) {
|
||||
@@ -387,7 +390,7 @@ func TestManager_RotateSnapshots_DeleteError(t *testing.T) {
|
||||
os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second))
|
||||
}
|
||||
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// Stub removeFileFunc to return error for specific path
|
||||
origRemove := removeFileFunc
|
||||
removeFileFunc = func(p string) error {
|
||||
@@ -416,12 +419,12 @@ func TestManager_ApplyConfig_GenerateConfigFails(t *testing.T) {
|
||||
|
||||
// stub generateConfigFunc to always return error
|
||||
orig := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool) (*Config, error) {
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool) (*Config, error) {
|
||||
return nil, fmt.Errorf("generate fail")
|
||||
}
|
||||
defer func() { generateConfigFunc = orig }()
|
||||
|
||||
manager := NewManager(nil, db, tmp, "", false)
|
||||
manager := NewManager(nil, db, tmp, "", false, config.SecurityConfig{})
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "generate config")
|
||||
@@ -461,7 +464,7 @@ func TestManager_ApplyConfig_ValidateFails(t *testing.T) {
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmp, "", false)
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{})
|
||||
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
@@ -470,7 +473,7 @@ func TestManager_ApplyConfig_ValidateFails(t *testing.T) {
|
||||
|
||||
func TestManager_Rollback_ReadFileError(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
manager := NewManager(nil, nil, tmp, "", false)
|
||||
manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{})
|
||||
// Create snapshot entries via write
|
||||
p := filepath.Join(tmp, "config-123.json")
|
||||
os.WriteFile(p, []byte(`{"apps":{"http":{}}}`), 0644)
|
||||
@@ -514,8 +517,164 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning_Stderr(t *testing.T) {
|
||||
defer func() { readDirFunc = origReadDir }()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, t.TempDir(), "", false)
|
||||
manager := NewManager(client, db, t.TempDir(), "", false, config.SecurityConfig{})
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
// Should succeed despite rotation warning (non-fatal)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_ReappliesOnFlagChange(t *testing.T) {
|
||||
// Capture /load payloads
|
||||
loadCh := make(chan []byte, 10)
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == http.MethodPost {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
loadCh <- body
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
// Setup DB
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.AccessList{}))
|
||||
|
||||
// Create an AccessList attached to host
|
||||
acl := models.AccessList{UUID: "acl-1", Name: "test-acl", Type: "whitelist", IPRules: `[{"cidr":"127.0.0.1/32"}]`, Enabled: true}
|
||||
assert.NoError(t, db.Create(&acl).Error)
|
||||
|
||||
host := models.ProxyHost{DomainNames: "flag.example.com", ForwardHost: "127.0.0.1", ForwardPort: 8080, Enabled: true}
|
||||
assert.NoError(t, db.Create(&host).Error)
|
||||
// Update with access list FK
|
||||
assert.NoError(t, db.Model(&host).Update("access_list_id", acl.ID).Error)
|
||||
|
||||
// Ensure DB setting is not present so ACL disabled by default
|
||||
// Manager default SecurityConfig has ACLMode disabled
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "disabled", WAFMode: "disabled", RateLimitMode: "disabled", CrowdSecMode: "disabled"}
|
||||
manager := NewManager(client, db, tmpDir, "", false, secCfg)
|
||||
|
||||
// First ApplyConfig - ACL disabled, we expect no ACL-related static_response
|
||||
assert.NoError(t, manager.ApplyConfig(context.Background()))
|
||||
var body1 []byte
|
||||
select {
|
||||
case body1 = <-loadCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for /load request 1")
|
||||
}
|
||||
var cfg1 Config
|
||||
assert.NoError(t, json.Unmarshal(body1, &cfg1))
|
||||
// Ensure not present — find the host route and check handles
|
||||
found := false
|
||||
for _, r := range cfg1.Apps.HTTP.Servers["charon_server"].Routes {
|
||||
for _, m := range r.Match {
|
||||
for _, h := range m.Host {
|
||||
if h == "flag.example.com" {
|
||||
for _, handle := range r.Handle {
|
||||
if handlerName, ok := handle["handler"].(string); ok && handlerName == "subroute" {
|
||||
if routes, ok := handle["routes"].([]interface{}); ok {
|
||||
for _, rt := range routes {
|
||||
if rtMap, ok := rt.(map[string]interface{}); ok {
|
||||
if inner, ok := rtMap["handle"].([]interface{}); ok {
|
||||
for _, itm := range inner {
|
||||
if itmMap, ok := itm.(map[string]interface{}); ok {
|
||||
if body, ok := itmMap["body"].(string); ok {
|
||||
if strings.Contains(body, "Access denied") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.False(t, found, "ACL handler must not be present when ACL disabled")
|
||||
|
||||
// Enable ACL via DB runtime setting
|
||||
assert.NoError(t, db.Create(&models.Setting{Key: "security.acl.enabled", Value: "true"}).Error)
|
||||
// Next ApplyConfig - ACL enabled
|
||||
assert.NoError(t, manager.ApplyConfig(context.Background()))
|
||||
var body2 []byte
|
||||
select {
|
||||
case body2 = <-loadCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for /load request 2")
|
||||
}
|
||||
var cfg2 Config
|
||||
assert.NoError(t, json.Unmarshal(body2, &cfg2))
|
||||
found = false
|
||||
for _, r := range cfg2.Apps.HTTP.Servers["charon_server"].Routes {
|
||||
for _, m := range r.Match {
|
||||
for _, h := range m.Host {
|
||||
if h == "flag.example.com" {
|
||||
for _, handle := range r.Handle {
|
||||
if handlerName, ok := handle["handler"].(string); ok && handlerName == "subroute" {
|
||||
if routes, ok := handle["routes"].([]interface{}); ok {
|
||||
for _, rt := range routes {
|
||||
if rtMap, ok := rt.(map[string]interface{}); ok {
|
||||
if inner, ok := rtMap["handle"].([]interface{}); ok {
|
||||
for _, itm := range inner {
|
||||
if itmMap, ok := itm.(map[string]interface{}); ok {
|
||||
if body, ok := itmMap["body"].(string); ok {
|
||||
if strings.Contains(body, "Access denied") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Logf("body2: %s", string(body2))
|
||||
}
|
||||
assert.True(t, found, "ACL handler must be present when ACL enabled via DB")
|
||||
|
||||
// Enable CrowdSec via DB runtime setting (switch from disabled to local)
|
||||
assert.NoError(t, db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "local"}).Error)
|
||||
// Next ApplyConfig - CrowdSec enabled
|
||||
assert.NoError(t, manager.ApplyConfig(context.Background()))
|
||||
var body3 []byte
|
||||
select {
|
||||
case body3 = <-loadCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for /load request 3")
|
||||
}
|
||||
var cfg3 Config
|
||||
assert.NoError(t, json.Unmarshal(body3, &cfg3))
|
||||
// For crowdsec handler we inserted a simple Handler{"handler":"crowdsec"}
|
||||
hasCrowdsec := false
|
||||
for _, r := range cfg3.Apps.HTTP.Servers["charon_server"].Routes {
|
||||
for _, m := range r.Match {
|
||||
for _, h := range m.Host {
|
||||
if h == "flag.example.com" {
|
||||
for _, handle := range r.Handle {
|
||||
if handlerName, ok := handle["handler"].(string); ok && handlerName == "crowdsec" {
|
||||
hasCrowdsec = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasCrowdsec, "CrowdSec handler should be present when enabled via DB")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -45,7 +46,7 @@ func TestManager_ApplyConfig(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Create a host
|
||||
host := models.ProxyHost{
|
||||
@@ -82,7 +83,7 @@ func TestManager_ApplyConfig_Failure(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Create a host
|
||||
host := models.ProxyHost{
|
||||
@@ -117,7 +118,7 @@ func TestManager_Ping(t *testing.T) {
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, nil, "", "", false)
|
||||
manager := NewManager(client, nil, "", "", false, config.SecurityConfig{})
|
||||
|
||||
err := manager.Ping(context.Background())
|
||||
assert.NoError(t, err)
|
||||
@@ -136,7 +137,7 @@ func TestManager_GetCurrentConfig(t *testing.T) {
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, nil, "", "", false)
|
||||
manager := NewManager(client, nil, "", "", false, config.SecurityConfig{})
|
||||
|
||||
config, err := manager.GetCurrentConfig(context.Background())
|
||||
assert.NoError(t, err)
|
||||
@@ -161,7 +162,7 @@ func TestManager_RotateSnapshots(t *testing.T) {
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}))
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Create 15 dummy config files
|
||||
for i := 0; i < 15; i++ {
|
||||
@@ -217,7 +218,7 @@ func TestManager_Rollback_Success(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// 1. Apply valid config (creates snapshot)
|
||||
host1 := models.ProxyHost{
|
||||
@@ -266,7 +267,7 @@ func TestManager_ApplyConfig_DBError(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient("http://localhost")
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Close DB to force error
|
||||
sqlDB, _ := db.DB()
|
||||
@@ -290,7 +291,7 @@ func TestManager_ApplyConfig_ValidationError(t *testing.T) {
|
||||
os.WriteFile(configDir, []byte("not a dir"), 0644)
|
||||
|
||||
client := NewClient("http://localhost")
|
||||
manager := NewManager(client, db, configDir, "", false)
|
||||
manager := NewManager(client, db, configDir, "", false, config.SecurityConfig{})
|
||||
|
||||
host := models.ProxyHost{
|
||||
DomainNames: "example.com",
|
||||
@@ -320,7 +321,7 @@ func TestManager_Rollback_Failure(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir, "", false)
|
||||
manager := NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
|
||||
|
||||
// Create a dummy snapshot manually so rollback has something to try
|
||||
os.WriteFile(filepath.Join(tmpDir, "config-123.json"), []byte("{}"), 0644)
|
||||
@@ -330,3 +331,131 @@ func TestManager_Rollback_Failure(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "rollback also failed")
|
||||
}
|
||||
|
||||
func TestComputeEffectiveFlags_DefaultsNoDB(t *testing.T) {
|
||||
// No DB - rely on SecurityConfig defaults only
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
|
||||
manager := NewManager(nil, nil, "", "", false, secCfg)
|
||||
|
||||
cerb, acl, waf, rl, cs := manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, cerb)
|
||||
require.True(t, acl)
|
||||
require.True(t, waf)
|
||||
require.True(t, rl)
|
||||
require.True(t, cs)
|
||||
|
||||
// If Cerberus disabled, all subcomponents must be disabled
|
||||
secCfg.CerberusEnabled = false
|
||||
manager = NewManager(nil, nil, "", "", false, secCfg)
|
||||
cerb, acl, waf, rl, cs = manager.computeEffectiveFlags(context.Background())
|
||||
require.False(t, cerb)
|
||||
require.False(t, acl)
|
||||
require.False(t, waf)
|
||||
require.False(t, rl)
|
||||
require.False(t, cs)
|
||||
|
||||
// CrowdSec external mode should disable CrowdSec in computed flags
|
||||
secCfg = config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "external"}
|
||||
manager = NewManager(nil, nil, "", "", false, secCfg)
|
||||
cerb, acl, waf, rl, cs = manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, cerb)
|
||||
require.True(t, acl)
|
||||
require.True(t, waf)
|
||||
require.True(t, rl)
|
||||
require.False(t, cs)
|
||||
}
|
||||
|
||||
// Removed combined DB overrides test - replaced by smaller, focused DB tests
|
||||
|
||||
func TestComputeEffectiveFlags_DB_CerberusDisabled(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
// Set runtime override to disable cerberus
|
||||
res := db.Create(&models.Setting{Key: "security.cerberus.enabled", Value: "false"})
|
||||
require.NoError(t, res.Error)
|
||||
|
||||
cerb, acl, waf, rl, cs := manager.computeEffectiveFlags(context.Background())
|
||||
require.False(t, cerb)
|
||||
require.False(t, acl)
|
||||
require.False(t, waf)
|
||||
require.False(t, rl)
|
||||
require.False(t, cs)
|
||||
}
|
||||
|
||||
// TestComputeEffectiveFlags_DB_ACLDisables: replaced by TestComputeEffectiveFlags_DB_ACLTrueAndFalse
|
||||
// TestComputeEffectiveFlags_DB_ACLDisables: Replaced by focused tests TestComputeEffectiveFlags_DB_ACLTrueAndFalse
|
||||
|
||||
func TestComputeEffectiveFlags_DB_CrowdSecExternal(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "external"})
|
||||
require.NoError(t, res.Error)
|
||||
|
||||
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
|
||||
require.False(t, cs)
|
||||
}
|
||||
|
||||
func TestComputeEffectiveFlags_DB_CrowdSecUnknown(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "unknown"})
|
||||
require.NoError(t, res.Error)
|
||||
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
|
||||
require.False(t, cs)
|
||||
}
|
||||
|
||||
func TestComputeEffectiveFlags_DB_CrowdSecLocal(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled", WAFMode: "enabled", RateLimitMode: "enabled", CrowdSecMode: "local"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
res := db.Create(&models.Setting{Key: "security.crowdsec.mode", Value: "local"})
|
||||
require.NoError(t, res.Error)
|
||||
_, _, _, _, cs := manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, cs)
|
||||
}
|
||||
|
||||
func TestComputeEffectiveFlags_DB_ACLTrueAndFalse(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.Setting{}))
|
||||
|
||||
secCfg := config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled"}
|
||||
manager := NewManager(nil, db, "", "", false, secCfg)
|
||||
|
||||
// Set acl true
|
||||
res := db.Create(&models.Setting{Key: "security.acl.enabled", Value: "true"})
|
||||
require.NoError(t, res.Error)
|
||||
_, acl, _, _, _ := manager.computeEffectiveFlags(context.Background())
|
||||
require.True(t, acl)
|
||||
|
||||
// Set acl false
|
||||
db.Where("key = ?", "security.acl.enabled").Delete(&models.Setting{})
|
||||
res = db.Create(&models.Setting{Key: "security.acl.enabled", Value: "false"})
|
||||
require.NoError(t, res.Error)
|
||||
_, acl, _, _, _ = manager.computeEffectiveFlags(context.Background())
|
||||
require.False(t, acl)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestValidate_ValidConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false)
|
||||
config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false)
|
||||
err := Validate(config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user