- Implemented `useManualChallenge`, `useChallengePoll`, and `useManualChallengeMutations` hooks for managing manual DNS challenges. - Created tests for the `useManualChallenge` hooks to ensure correct fetching and mutation behavior. - Added `ManualDNSChallenge` component for displaying challenge details and actions. - Developed end-to-end tests for the Manual DNS Provider feature, covering provider selection, challenge UI, and accessibility compliance. - Included error handling tests for verification failures and network errors.
927 lines
28 KiB
Go
927 lines
28 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/crypto"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupEncryptionTestDB(t *testing.T) *gorm.DB {
|
|
// Use a unique file-based database for each test to avoid sharing state
|
|
dbPath := fmt.Sprintf("/tmp/test_encryption_%d.db", time.Now().UnixNano())
|
|
t.Cleanup(func() {
|
|
_ = os.Remove(dbPath)
|
|
})
|
|
|
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
|
// Disable prepared statements for SQLite to avoid issues
|
|
PrepareStmt: false,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Migrate all required tables
|
|
err = db.AutoMigrate(&models.DNSProvider{}, &models.SecurityAudit{})
|
|
require.NoError(t, err)
|
|
|
|
return db
|
|
}
|
|
|
|
func setupEncryptionTestRouter(handler *EncryptionHandler, isAdmin bool) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
// Mock admin middleware
|
|
router.Use(func(c *gin.Context) {
|
|
if isAdmin {
|
|
c.Set("user_role", "admin")
|
|
c.Set("user_id", uint(1))
|
|
}
|
|
c.Next()
|
|
})
|
|
|
|
api := router.Group("/api/v1/admin/encryption")
|
|
{
|
|
api.GET("/status", handler.GetStatus)
|
|
api.POST("/rotate", handler.Rotate)
|
|
api.GET("/history", handler.GetHistory)
|
|
api.POST("/validate", handler.Validate)
|
|
}
|
|
|
|
return router
|
|
}
|
|
|
|
func TestEncryptionHandler_GetStatus(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Generate test keys
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
|
|
t.Run("admin can get status", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var status crypto.RotationStatus
|
|
err := json.Unmarshal(w.Body.Bytes(), &status)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 1, status.CurrentVersion)
|
|
assert.False(t, status.NextKeyConfigured)
|
|
assert.Equal(t, 0, status.LegacyKeyCount)
|
|
})
|
|
|
|
t.Run("non-admin cannot get status", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("status shows next key when configured", func(t *testing.T) {
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") }()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var status crypto.RotationStatus
|
|
err = json.Unmarshal(w.Body.Bytes(), &status)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, status.NextKeyConfigured)
|
|
})
|
|
|
|
t.Run("status error when database unavailable", func(t *testing.T) {
|
|
// Close the database to trigger an error
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
_ = sqlDB.Close()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "error")
|
|
})
|
|
}
|
|
|
|
func TestEncryptionHandler_Rotate(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Generate test keys
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer func() {
|
|
_ = os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
_ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
}()
|
|
|
|
// Create test providers
|
|
currentService, err := crypto.NewEncryptionService(currentKey)
|
|
require.NoError(t, err)
|
|
|
|
credentials := map[string]string{"api_key": "test123"}
|
|
credJSON, _ := json.Marshal(credentials)
|
|
encrypted, _ := currentService.Encrypt(credJSON)
|
|
|
|
provider := models.DNSProvider{
|
|
Name: "Test Provider",
|
|
ProviderType: "cloudflare",
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&provider).Error)
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
|
|
t.Run("admin can trigger rotation", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Flush async audit logging
|
|
securityService.Flush()
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result crypto.RotationResult
|
|
err := json.Unmarshal(w.Body.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 1, result.TotalProviders)
|
|
assert.Equal(t, 1, result.SuccessCount)
|
|
assert.Equal(t, 0, result.FailureCount)
|
|
assert.Equal(t, 2, result.NewKeyVersion)
|
|
assert.NotEmpty(t, result.Duration)
|
|
|
|
// Verify audit logs were created
|
|
var audits []models.SecurityAudit
|
|
db.Where("event_category = ?", "encryption").Find(&audits)
|
|
assert.GreaterOrEqual(t, len(audits), 2) // start + completion
|
|
})
|
|
|
|
t.Run("non-admin cannot trigger rotation", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("rotation fails without next key", func(t *testing.T) {
|
|
_ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
defer func() { _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) }()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "CHARON_ENCRYPTION_KEY_NEXT not configured")
|
|
})
|
|
}
|
|
|
|
func TestEncryptionHandler_GetHistory(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
// Create sample audit logs
|
|
for i := 0; i < 5; i++ {
|
|
audit := &models.SecurityAudit{
|
|
Actor: "admin",
|
|
Action: "encryption_key_rotation_completed",
|
|
EventCategory: "encryption",
|
|
Details: "{}",
|
|
}
|
|
_ = securityService.LogAudit(audit)
|
|
}
|
|
|
|
// Flush async audit logging
|
|
securityService.Flush()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
|
|
t.Run("admin can get history", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/history", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "audits")
|
|
assert.Contains(t, response, "total")
|
|
assert.Contains(t, response, "page")
|
|
assert.Contains(t, response, "limit")
|
|
})
|
|
|
|
t.Run("non-admin cannot get history", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/history", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("supports pagination", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/history?page=1&limit=2", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, float64(1), response["page"])
|
|
assert.Equal(t, float64(2), response["limit"])
|
|
})
|
|
|
|
t.Run("history error when service fails", func(t *testing.T) {
|
|
// Create a new DB that will be closed to trigger error
|
|
dbPath := fmt.Sprintf("/tmp/test_encryption_fail_%d.db", time.Now().UnixNano())
|
|
defer func() { _ = os.Remove(dbPath) }()
|
|
|
|
failDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{PrepareStmt: false})
|
|
require.NoError(t, err)
|
|
require.NoError(t, failDB.AutoMigrate(&models.SecurityAudit{}))
|
|
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }()
|
|
|
|
rotationService, err := crypto.NewRotationService(failDB)
|
|
require.NoError(t, err)
|
|
|
|
failSecurityService := services.NewSecurityService(failDB)
|
|
|
|
// Close the database to trigger errors
|
|
sqlDB, err := failDB.DB()
|
|
require.NoError(t, err)
|
|
_ = sqlDB.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, failSecurityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/history", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "error")
|
|
|
|
failSecurityService.Close()
|
|
})
|
|
}
|
|
|
|
func TestEncryptionHandler_Validate(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
|
|
t.Run("admin can validate keys", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Flush async audit logging
|
|
securityService.Flush()
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, response["valid"].(bool))
|
|
assert.Contains(t, response, "message")
|
|
|
|
// Verify audit log was created
|
|
var audits []models.SecurityAudit
|
|
db.Where("action = ?", "encryption_key_validation_success").Find(&audits)
|
|
assert.Greater(t, len(audits), 0)
|
|
})
|
|
|
|
t.Run("non-admin cannot validate keys", func(t *testing.T) {
|
|
router := setupEncryptionTestRouter(handler, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("validation fails with invalid key configuration", func(t *testing.T) {
|
|
// Unset the encryption key to trigger validation failure
|
|
_ = os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
defer func() { _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) }()
|
|
|
|
// Create rotation service with no key configured
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
// This should fail, but if it doesn't, we still test the validation endpoint
|
|
if err != nil {
|
|
// Expected: NewRotationService fails without a key
|
|
return
|
|
}
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
securityService.Flush()
|
|
|
|
// Should return bad request with validation error
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, response["valid"].(bool))
|
|
assert.Contains(t, response, "error")
|
|
|
|
// Verify audit log for validation failure was created
|
|
var audits []models.SecurityAudit
|
|
db.Where("action = ?", "encryption_key_validation_failed").Find(&audits)
|
|
assert.Greater(t, len(audits), 0)
|
|
})
|
|
}
|
|
|
|
func TestEncryptionHandler_IntegrationFlow(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Setup: Generate keys
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }()
|
|
|
|
// Create initial provider
|
|
currentService, err := crypto.NewEncryptionService(currentKey)
|
|
require.NoError(t, err)
|
|
|
|
credentials := map[string]string{"api_key": "secret123"}
|
|
credJSON, _ := json.Marshal(credentials)
|
|
encrypted, _ := currentService.Encrypt(credJSON)
|
|
|
|
provider := models.DNSProvider{
|
|
Name: "Integration Test Provider",
|
|
ProviderType: "cloudflare",
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&provider).Error)
|
|
|
|
t.Run("complete rotation workflow", func(t *testing.T) {
|
|
// Step 1: Check initial status
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
securityService := services.NewSecurityService(db)
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Step 2: Validate current configuration
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Step 3: Configure next key
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
|
|
// Reinitialize rotation service to pick up new key
|
|
// Keep using the same SecurityService and database
|
|
rotationService, err = crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
handler = NewEncryptionHandler(rotationService, securityService)
|
|
router = setupEncryptionTestRouter(handler, true)
|
|
|
|
// Step 4: Trigger rotation
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Step 5: Verify rotation result
|
|
var result crypto.RotationResult
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, result.SuccessCount)
|
|
|
|
// Step 6: Check updated status
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Step 7: Verify history contains rotation events
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/api/v1/admin/encryption/history", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var historyResponse map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &historyResponse)
|
|
require.NoError(t, err)
|
|
if historyResponse["total"] != nil {
|
|
assert.Greater(t, int(historyResponse["total"].(float64)), 0)
|
|
}
|
|
|
|
// Clean up
|
|
securityService.Close()
|
|
})
|
|
}
|
|
|
|
// TestEncryptionHandler_HelperFunctions tests the isAdmin and getActorFromGinContext helpers
|
|
func TestEncryptionHandler_HelperFunctions(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
t.Run("isAdmin with invalid role type", func(t *testing.T) {
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_role", 12345) // Invalid type (int instead of string)
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
if isAdmin(c) {
|
|
c.JSON(http.StatusOK, gin.H{"admin": true})
|
|
} else {
|
|
c.JSON(http.StatusForbidden, gin.H{"admin": false})
|
|
}
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("getActorFromGinContext with string user_id", func(t *testing.T) {
|
|
router := gin.New()
|
|
var capturedActor string
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", "user-string-123")
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
capturedActor = getActorFromGinContext(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, "user-string-123", capturedActor)
|
|
})
|
|
|
|
t.Run("getActorFromGinContext with uint user_id", func(t *testing.T) {
|
|
router := gin.New()
|
|
var capturedActor string
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", uint(42))
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
capturedActor = getActorFromGinContext(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, "42", capturedActor)
|
|
})
|
|
|
|
t.Run("getActorFromGinContext without user_id returns system", func(t *testing.T) {
|
|
router := gin.New()
|
|
var capturedActor string
|
|
router.GET("/test", func(c *gin.Context) {
|
|
capturedActor = getActorFromGinContext(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, "system", capturedActor)
|
|
})
|
|
}
|
|
|
|
// TestEncryptionHandler_RefreshKey_RotatesCredentials tests key rotation for credentials
|
|
func TestEncryptionHandler_RefreshKey_RotatesCredentials(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Generate test keys
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer func() {
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
}()
|
|
|
|
// Create test provider with encrypted credentials
|
|
currentService, err := crypto.NewEncryptionService(currentKey)
|
|
require.NoError(t, err)
|
|
|
|
credentials := map[string]string{"api_key": "test123"}
|
|
credJSON, _ := json.Marshal(credentials)
|
|
encrypted, _ := currentService.Encrypt(credJSON)
|
|
|
|
provider := models.DNSProvider{
|
|
Name: "Test Provider",
|
|
ProviderType: "cloudflare",
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&provider).Error)
|
|
|
|
// Initialize rotation service
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
// Trigger rotation
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result crypto.RotationResult
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 1, result.SuccessCount)
|
|
assert.Equal(t, 2, result.NewKeyVersion)
|
|
}
|
|
|
|
// TestEncryptionHandler_RefreshKey_FailsWithoutProvider tests rotation without next key
|
|
func TestEncryptionHandler_RefreshKey_FailsWithoutProvider(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Set only current key, no next key
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
// Attempt rotation without next key configured
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "CHARON_ENCRYPTION_KEY_NEXT not configured")
|
|
}
|
|
|
|
// TestEncryptionHandler_RefreshKey_InvalidOldKey tests rotation with mismatched old key
|
|
func TestEncryptionHandler_RefreshKey_InvalidOldKey(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Generate test keys
|
|
wrongKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
// Create provider with one key
|
|
correctKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
correctService, err := crypto.NewEncryptionService(correctKey)
|
|
require.NoError(t, err)
|
|
|
|
credentials := map[string]string{"api_key": "test123"}
|
|
credJSON, _ := json.Marshal(credentials)
|
|
encrypted, _ := correctService.Encrypt(credJSON)
|
|
|
|
provider := models.DNSProvider{
|
|
Name: "Test Provider",
|
|
ProviderType: "cloudflare",
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&provider).Error)
|
|
|
|
// Now set wrong key and try to rotate
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY", wrongKey)
|
|
_ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer func() {
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
}()
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
// Attempt rotation with wrong key
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
|
|
// Rotation may succeed but with failures for providers with wrong key
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result crypto.RotationResult
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
// Should have failure count > 0 due to decryption error
|
|
assert.Greater(t, result.FailureCount, 0)
|
|
}
|
|
|
|
// TestEncryptionHandler_GetActorFromGinContext_InvalidType tests getActorFromGinContext with invalid type
|
|
func TestEncryptionHandler_GetActorFromGinContext_InvalidType(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
var capturedActor string
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", int64(999)) // int64 instead of uint or string
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
capturedActor = getActorFromGinContext(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Invalid type should return "system" as fallback
|
|
assert.Equal(t, "system", capturedActor)
|
|
}
|
|
|
|
// TestEncryptionHandler_RotateWithPartialFailures tests rotation that has some successes and failures
|
|
func TestEncryptionHandler_RotateWithPartialFailures(t *testing.T) {
|
|
db := setupEncryptionTestDB(t)
|
|
|
|
// Generate test keys
|
|
currentKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
nextKey, err := crypto.GenerateNewKey()
|
|
require.NoError(t, err)
|
|
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey)
|
|
defer func() {
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT")
|
|
}()
|
|
|
|
// Create a valid provider
|
|
currentService, err := crypto.NewEncryptionService(currentKey)
|
|
require.NoError(t, err)
|
|
|
|
validCreds := map[string]string{"api_key": "valid123"}
|
|
credJSON, _ := json.Marshal(validCreds)
|
|
validEncrypted, _ := currentService.Encrypt(credJSON)
|
|
|
|
validProvider := models.DNSProvider{
|
|
UUID: "valid-provider-uuid",
|
|
Name: "Valid Provider",
|
|
ProviderType: "cloudflare",
|
|
CredentialsEncrypted: validEncrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&validProvider).Error)
|
|
|
|
// Create an invalid provider (corrupted encrypted data)
|
|
invalidProvider := models.DNSProvider{
|
|
UUID: "invalid-provider-uuid",
|
|
Name: "Invalid Provider",
|
|
ProviderType: "route53",
|
|
CredentialsEncrypted: "corrupted-data-that-cannot-be-decrypted",
|
|
KeyVersion: 1,
|
|
}
|
|
require.NoError(t, db.Create(&invalidProvider).Error)
|
|
|
|
rotationService, err := crypto.NewRotationService(db)
|
|
require.NoError(t, err)
|
|
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/rotate", nil)
|
|
router.ServeHTTP(w, req)
|
|
securityService.Flush()
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var result crypto.RotationResult
|
|
err = json.Unmarshal(w.Body.Bytes(), &result)
|
|
require.NoError(t, err)
|
|
|
|
// Should have at least 2 providers attempted
|
|
assert.Equal(t, 2, result.TotalProviders)
|
|
// Should have at least 1 success (valid provider)
|
|
assert.GreaterOrEqual(t, result.SuccessCount, 1)
|
|
// Should have at least 1 failure (invalid provider)
|
|
assert.GreaterOrEqual(t, result.FailureCount, 1)
|
|
// Failed providers list should be populated
|
|
assert.NotEmpty(t, result.FailedProviders)
|
|
}
|
|
|
|
// TestEncryptionHandler_isAdmin_NoRoleSet tests isAdmin when no role is set
|
|
func TestEncryptionHandler_isAdmin_NoRoleSet(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
// No middleware setting user_role
|
|
router.GET("/test", func(c *gin.Context) {
|
|
if isAdmin(c) {
|
|
c.JSON(http.StatusOK, gin.H{"admin": true})
|
|
} else {
|
|
c.JSON(http.StatusForbidden, gin.H{"admin": false})
|
|
}
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
// TestEncryptionHandler_isAdmin_NonAdminRole tests isAdmin with non-admin role
|
|
func TestEncryptionHandler_isAdmin_NonAdminRole(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_role", "user") // Regular user, not admin
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
if isAdmin(c) {
|
|
c.JSON(http.StatusOK, gin.H{"admin": true})
|
|
} else {
|
|
c.JSON(http.StatusForbidden, gin.H{"admin": false})
|
|
}
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|