chore: git cache cleanup
This commit is contained in:
605
backend/internal/api/handlers/system_permissions_handler_test.go
Normal file
605
backend/internal/api/handlers/system_permissions_handler_test.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/Wikid82/charon/backend/internal/util"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type stubPermissionChecker struct{}
|
||||
|
||||
type fakeNoStatFileInfo struct{}
|
||||
|
||||
func (fakeNoStatFileInfo) Name() string { return "fake" }
|
||||
func (fakeNoStatFileInfo) Size() int64 { return 0 }
|
||||
func (fakeNoStatFileInfo) Mode() os.FileMode { return 0 }
|
||||
func (fakeNoStatFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (fakeNoStatFileInfo) IsDir() bool { return false }
|
||||
func (fakeNoStatFileInfo) Sys() any { return nil }
|
||||
|
||||
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_NewDefaultsCheckerToOSChecker(t *testing.T) {
|
||||
h := NewSystemPermissionsHandler(config.Config{}, nil, nil)
|
||||
require.NotNil(t, h)
|
||||
require.NotNil(t, h.checker)
|
||||
}
|
||||
|
||||
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_RepairPermissions_NonAdmin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
h := NewSystemPermissionsHandler(config.Config{SingleContainer: true}, nil, stubPermissionChecker{})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("role", "user")
|
||||
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)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_RepairPermissions_InvalidJSONWhenRoot(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))
|
||||
|
||||
h := NewSystemPermissionsHandler(config.Config{
|
||||
SingleContainer: true,
|
||||
DatabasePath: filepath.Join(dataDir, "charon.db"),
|
||||
ConfigRoot: dataDir,
|
||||
CaddyLogDir: dataDir,
|
||||
CrowdSecLogDir: dataDir,
|
||||
}, 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_DefaultPathsAndAllowlistRoots(t *testing.T) {
|
||||
h := NewSystemPermissionsHandler(config.Config{
|
||||
DatabasePath: "/app/data/charon.db",
|
||||
ConfigRoot: "/app/config",
|
||||
CaddyLogDir: "/var/log/caddy",
|
||||
CrowdSecLogDir: "/var/log/crowdsec",
|
||||
PluginsDir: "/app/plugins",
|
||||
}, nil, stubPermissionChecker{})
|
||||
|
||||
paths := h.defaultPaths()
|
||||
require.Len(t, paths, 11)
|
||||
require.Equal(t, "/app/data", paths[0].Path)
|
||||
require.Equal(t, "/app/plugins", paths[len(paths)-1].Path)
|
||||
|
||||
roots := h.allowlistRoots()
|
||||
require.Equal(t, []string{"/app/data", "/app/config", "/var/log/caddy", "/var/log/crowdsec"}, roots)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_IsOwnedByFalseWhenSysNotStat(t *testing.T) {
|
||||
owned := isOwnedBy(fakeNoStatFileInfo{}, os.Geteuid(), os.Getegid())
|
||||
require.False(t, owned)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_IsWithinAllowlist_RelErrorBranch(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
inAllow := filepath.Join(tmp, "a", "b")
|
||||
require.NoError(t, os.MkdirAll(inAllow, 0o750))
|
||||
|
||||
badRoot := string([]byte{'/', 0, 'x'})
|
||||
allowed := isWithinAllowlist(inAllow, []string{badRoot, tmp})
|
||||
require.True(t, allowed)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_IsWithinAllowlist_AllRelErrorsReturnFalse(t *testing.T) {
|
||||
badRoot1 := string([]byte{'/', 0, 'x'})
|
||||
badRoot2 := string([]byte{'/', 0, 'y'})
|
||||
allowed := isWithinAllowlist("/tmp/some/path", []string{badRoot1, badRoot2})
|
||||
require.False(t, allowed)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUserID(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))
|
||||
|
||||
securitySvc := services.NewSecurityService(db)
|
||||
h := NewSystemPermissionsHandler(config.Config{}, securitySvc, stubPermissionChecker{})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("role", "admin")
|
||||
c.Set("userID", 42)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
h.logAudit(c, "permissions_diagnostics", "ok", "", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_LogAudit_PersistsAuditWithUnknownActor(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SecurityAudit{}))
|
||||
|
||||
securitySvc := services.NewSecurityService(db)
|
||||
h := NewSystemPermissionsHandler(config.Config{}, securitySvc, stubPermissionChecker{})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("role", "admin")
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/system/permissions", http.NoBody)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
h.logAudit(c, "permissions_diagnostics", "ok", "", 1)
|
||||
})
|
||||
}
|
||||
|
||||
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("outside allowlist rejected before stat for missing path", func(t *testing.T) {
|
||||
outsideMissing := filepath.Join(t.TempDir(), "missing.txt")
|
||||
|
||||
result := h.repairPath(outsideMissing, 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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_OSChecker_Check(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("test expects root-owned temp paths in CI")
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
filePath := filepath.Join(tmp, "check.txt")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("ok"), 0o600))
|
||||
|
||||
checker := OSChecker{}
|
||||
result := checker.Check(filePath, "rw")
|
||||
require.Equal(t, filePath, result.Path)
|
||||
require.Equal(t, "rw", result.Required)
|
||||
require.True(t, result.Exists)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_RepairPermissions_InvalidRequestBody_Root(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("test requires root execution")
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tmp := t.TempDir()
|
||||
dataDir := filepath.Join(tmp, "data")
|
||||
require.NoError(t, os.MkdirAll(dataDir, 0o750))
|
||||
|
||||
h := NewSystemPermissionsHandler(config.Config{
|
||||
SingleContainer: true,
|
||||
DatabasePath: filepath.Join(dataDir, "charon.db"),
|
||||
ConfigRoot: dataDir,
|
||||
CaddyLogDir: dataDir,
|
||||
CrowdSecLogDir: dataDir,
|
||||
PluginsDir: filepath.Join(tmp, "plugins"),
|
||||
}, nil, stubPermissionChecker{})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("role", "admin")
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/system/permissions/repair", bytes.NewBufferString(`{"group_mode":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.RepairPermissions(c)
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_RepairPath_LstatInvalidArgument(t *testing.T) {
|
||||
h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
|
||||
allowRoot := t.TempDir()
|
||||
|
||||
result := h.repairPath("/tmp/\x00invalid", false, []string{allowRoot})
|
||||
require.Equal(t, "error", result.Status)
|
||||
require.Equal(t, "permissions_outside_allowlist", result.ErrorCode)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_RepairPath_RepairedBranch(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("test requires root execution")
|
||||
}
|
||||
|
||||
h := NewSystemPermissionsHandler(config.Config{}, nil, stubPermissionChecker{})
|
||||
allowRoot := t.TempDir()
|
||||
targetFile := filepath.Join(allowRoot, "needs-repair.txt")
|
||||
require.NoError(t, os.WriteFile(targetFile, []byte("ok"), 0o600))
|
||||
|
||||
result := h.repairPath(targetFile, true, []string{allowRoot})
|
||||
require.Equal(t, "repaired", result.Status)
|
||||
require.Equal(t, "0660", result.ModeAfter)
|
||||
|
||||
info, err := os.Stat(targetFile)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, os.FileMode(0o660), info.Mode().Perm())
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_NormalizePath_ParentRefBranches(t *testing.T) {
|
||||
clean, code := normalizePath("/../etc")
|
||||
require.Equal(t, "/etc", clean)
|
||||
require.Empty(t, code)
|
||||
|
||||
clean, code = normalizePath("/var/../etc")
|
||||
require.Equal(t, "/etc", clean)
|
||||
require.Empty(t, code)
|
||||
}
|
||||
|
||||
func TestSystemPermissionsHandler_NormalizeAllowlist(t *testing.T) {
|
||||
allowlist := normalizeAllowlist([]string{"", "/tmp/data/..", "/var/log/charon"})
|
||||
require.Equal(t, []string{"/tmp", "/var/log/charon"}, allowlist)
|
||||
}
|
||||
Reference in New Issue
Block a user