Files
Charon/backend/internal/api/handlers/system_permissions_handler_test.go
GitHub Actions 716ec91f8f chore: Enhance test coverage across various handlers and services
- Added tests for transient SQLite errors in emergency_handler_test.go.
- Introduced validation tests for provider errors in notification_provider_handler_validation_test.go.
- Implemented helper tests for settings handling in settings_handler_helpers_test.go.
- Expanded backup_handler_test.go to include SQLite database setup and validation.
- Improved system_permissions_handler_test.go with additional path repair tests.
- Updated backup_service_test.go to ensure proper database handling and error checks during backup operations.
- Refined import_handler_test.go with additional session validation tests.
2026-02-16 20:32:16 +00:00

363 lines
11 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/util"
)
type stubPermissionChecker struct{}
func (stubPermissionChecker) Check(path, required string) util.PermissionCheck {
return util.PermissionCheck{
Path: path,
Required: required,
Exists: true,
Writable: true,
OwnerUID: 1000,
OwnerGID: 1000,
Mode: "0755",
}
}
func TestSystemPermissionsHandler_GetPermissions_Admin(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := config.Config{
DatabasePath: "/app/data/charon.db",
ConfigRoot: "/config",
CaddyLogDir: "/var/log/caddy",
CrowdSecLogDir: "/var/log/crowdsec",
PluginsDir: "/app/plugins",
}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)
h.GetPermissions(c)
require.Equal(t, http.StatusOK, w.Code)
var payload struct {
Paths []map[string]any `json:"paths"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.NotEmpty(t, payload.Paths)
first := payload.Paths[0]
require.NotEmpty(t, first["path"])
require.NotEmpty(t, first["required"])
require.NotEmpty(t, first["mode"])
}
func TestSystemPermissionsHandler_GetPermissions_NonAdmin(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := config.Config{}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "user")
c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)
h.GetPermissions(c)
require.Equal(t, http.StatusForbidden, w.Code)
var payload map[string]string
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Equal(t, "permissions_admin_only", payload["error_code"])
}
func TestSystemPermissionsHandler_RepairPermissions_NonRoot(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("test requires non-root execution")
}
gin.SetMode(gin.TestMode)
cfg := config.Config{SingleContainer: true}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", http.NoBody)
h.RepairPermissions(c)
require.Equal(t, http.StatusForbidden, w.Code)
var payload map[string]string
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Equal(t, "permissions_non_root", payload["error_code"])
}
func TestSystemPermissionsHandler_HelperFunctions(t *testing.T) {
t.Run("normalizePath", func(t *testing.T) {
clean, code := normalizePath("/tmp/example")
require.Equal(t, "/tmp/example", clean)
require.Empty(t, code)
clean, code = normalizePath("")
require.Empty(t, clean)
require.Equal(t, "permissions_invalid_path", code)
clean, code = normalizePath("relative/path")
require.Empty(t, clean)
require.Equal(t, "permissions_invalid_path", code)
})
t.Run("containsParentReference", func(t *testing.T) {
require.True(t, containsParentReference(".."))
require.True(t, containsParentReference("../secrets"))
require.True(t, containsParentReference("/var/../etc"))
require.True(t, containsParentReference("/var/log/.."))
require.False(t, containsParentReference("/var/log/charon"))
})
t.Run("isWithinAllowlist", func(t *testing.T) {
allowlist := []string{"/app/data", "/config"}
require.True(t, isWithinAllowlist("/app/data/charon.db", allowlist))
require.True(t, isWithinAllowlist("/config/caddy", allowlist))
require.False(t, isWithinAllowlist("/etc/passwd", allowlist))
})
t.Run("targetMode", func(t *testing.T) {
require.Equal(t, "0700", targetMode(true, false))
require.Equal(t, "0770", targetMode(true, true))
require.Equal(t, "0600", targetMode(false, false))
require.Equal(t, "0660", targetMode(false, true))
})
t.Run("parseMode", func(t *testing.T) {
mode, err := parseMode("0640")
require.NoError(t, err)
require.Equal(t, os.FileMode(0640), mode)
_, err = parseMode("")
require.Error(t, err)
_, err = parseMode("invalid")
require.Error(t, err)
})
t.Run("mapRepairErrorCode", func(t *testing.T) {
require.Equal(t, "", mapRepairErrorCode(nil))
require.Equal(t, "permissions_readonly", mapRepairErrorCode(syscall.EROFS))
require.Equal(t, "permissions_write_denied", mapRepairErrorCode(syscall.EACCES))
require.Equal(t, "permissions_repair_failed", mapRepairErrorCode(syscall.EINVAL))
})
}
func TestSystemPermissionsHandler_PathHasSymlink(t *testing.T) {
root := t.TempDir()
realDir := filepath.Join(root, "real")
require.NoError(t, os.Mkdir(realDir, 0o750))
plainPath := filepath.Join(realDir, "file.txt")
require.NoError(t, os.WriteFile(plainPath, []byte("ok"), 0o600))
hasSymlink, err := pathHasSymlink(plainPath)
require.NoError(t, err)
require.False(t, hasSymlink)
linkDir := filepath.Join(root, "link")
require.NoError(t, os.Symlink(realDir, linkDir))
symlinkedPath := filepath.Join(linkDir, "file.txt")
hasSymlink, err = pathHasSymlink(symlinkedPath)
require.NoError(t, err)
require.True(t, hasSymlink)
_, err = pathHasSymlink(filepath.Join(root, "missing", "file.txt"))
require.Error(t, err)
}
func TestSystemPermissionsHandler_RepairPermissions_DisabledWhenNotSingleContainer(t *testing.T) {
gin.SetMode(gin.TestMode)
h := NewSystemPermissionsHandler(config.Config{SingleContainer: false}, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":["/tmp"]}`))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusForbidden, w.Code)
var payload map[string]string
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Equal(t, "permissions_repair_disabled", payload["error_code"])
}
func TestSystemPermissionsHandler_RepairPermissions_InvalidJSON(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root execution")
}
gin.SetMode(gin.TestMode)
root := t.TempDir()
dataDir := filepath.Join(root, "data")
require.NoError(t, os.MkdirAll(dataDir, 0o750))
cfg := config.Config{
SingleContainer: true,
DatabasePath: filepath.Join(dataDir, "charon.db"),
ConfigRoot: dataDir,
CaddyLogDir: dataDir,
CrowdSecLogDir: dataDir,
PluginsDir: filepath.Join(root, "plugins"),
}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"paths":`))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSystemPermissionsHandler_RepairPermissions_Success(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("test requires root execution")
}
gin.SetMode(gin.TestMode)
root := t.TempDir()
dataDir := filepath.Join(root, "data")
require.NoError(t, os.MkdirAll(dataDir, 0o750))
targetFile := filepath.Join(dataDir, "repair-target.txt")
require.NoError(t, os.WriteFile(targetFile, []byte("repair"), 0o600))
cfg := config.Config{
SingleContainer: true,
DatabasePath: filepath.Join(dataDir, "charon.db"),
ConfigRoot: dataDir,
CaddyLogDir: dataDir,
CrowdSecLogDir: dataDir,
PluginsDir: filepath.Join(root, "plugins"),
}
h := NewSystemPermissionsHandler(cfg, nil, stubPermissionChecker{})
body := fmt.Sprintf(`{"paths":[%q],"group_mode":false}`, targetFile)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("role", "admin")
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
h.RepairPermissions(c)
require.Equal(t, http.StatusOK, w.Code)
var payload struct {
Paths []permissionsRepairResult `json:"paths"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
require.Len(t, payload.Paths, 1)
require.Equal(t, targetFile, payload.Paths[0].Path)
require.NotEqual(t, "error", payload.Paths[0].Status)
}
func TestSystemPermissionsHandler_RepairPath_Branches(t *testing.T) {
h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
allowRoot := t.TempDir()
allowlist := []string{allowRoot}
t.Run("invalid path", func(t *testing.T) {
result := h.repairPath("", false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_invalid_path", result.ErrorCode)
})
t.Run("missing path", func(t *testing.T) {
missingPath := filepath.Join(allowRoot, "missing-file.txt")
result := h.repairPath(missingPath, false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_missing_path", result.ErrorCode)
})
t.Run("symlink leaf rejected", func(t *testing.T) {
target := filepath.Join(allowRoot, "target.txt")
require.NoError(t, os.WriteFile(target, []byte("ok"), 0o600))
link := filepath.Join(allowRoot, "link.txt")
require.NoError(t, os.Symlink(target, link))
result := h.repairPath(link, false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_symlink_rejected", result.ErrorCode)
})
t.Run("symlink component rejected", func(t *testing.T) {
realDir := filepath.Join(allowRoot, "real")
require.NoError(t, os.MkdirAll(realDir, 0o750))
realFile := filepath.Join(realDir, "file.txt")
require.NoError(t, os.WriteFile(realFile, []byte("ok"), 0o600))
linkDir := filepath.Join(allowRoot, "linkdir")
require.NoError(t, os.Symlink(realDir, linkDir))
result := h.repairPath(filepath.Join(linkDir, "file.txt"), false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_symlink_rejected", result.ErrorCode)
})
t.Run("outside allowlist rejected", func(t *testing.T) {
outsideFile := filepath.Join(t.TempDir(), "outside.txt")
require.NoError(t, os.WriteFile(outsideFile, []byte("x"), 0o600))
result := h.repairPath(outsideFile, false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_outside_allowlist", result.ErrorCode)
})
t.Run("unsupported type rejected", func(t *testing.T) {
fifoPath := filepath.Join(allowRoot, "fifo")
require.NoError(t, syscall.Mkfifo(fifoPath, 0o600))
result := h.repairPath(fifoPath, false, allowlist)
require.Equal(t, "error", result.Status)
require.Equal(t, "permissions_unsupported_type", result.ErrorCode)
})
t.Run("already correct skipped", func(t *testing.T) {
filePath := filepath.Join(allowRoot, "already-correct.txt")
require.NoError(t, os.WriteFile(filePath, []byte("ok"), 0o600))
result := h.repairPath(filePath, false, allowlist)
require.Equal(t, "skipped", result.Status)
require.Equal(t, "permissions_repair_skipped", result.ErrorCode)
require.Equal(t, "0600", result.ModeAfter)
})
}