chore: add unit tests for system permissions handler and proxy host service validation
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -105,3 +109,182 @@ func TestSystemPermissionsHandler_RepairPermissions_NonRoot(t *testing.T) {
|
||||
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, 0o755))
|
||||
|
||||
plainPath := filepath.Join(realDir, "file.txt")
|
||||
require.NoError(t, os.WriteFile(plainPath, []byte("ok"), 0o644))
|
||||
|
||||
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, 0o755))
|
||||
|
||||
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, 0o755))
|
||||
|
||||
targetFile := filepath.Join(dataDir, "repair-target.txt")
|
||||
require.NoError(t, os.WriteFile(targetFile, []byte("repair"), 0o644))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -198,3 +198,33 @@ func TestProxyHostService_DNSChallengeValidation(t *testing.T) {
|
||||
assert.Equal(t, "localhost", persisted.ForwardHost)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProxyHostService_ValidateHostname(t *testing.T) {
|
||||
db := setupProxyHostTestDB(t)
|
||||
service := NewProxyHostService(db)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "plain hostname", host: "example.com", wantErr: false},
|
||||
{name: "hostname with scheme", host: "https://example.com", wantErr: false},
|
||||
{name: "hostname with port", host: "example.com:8080", wantErr: false},
|
||||
{name: "ipv4 address", host: "127.0.0.1", wantErr: false},
|
||||
{name: "bracketed ipv6 with port", host: "[::1]:443", wantErr: false},
|
||||
{name: "docker style underscore", host: "my_service", wantErr: false},
|
||||
{name: "invalid character", host: "invalid$host", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := service.ValidateHostname(tt.host)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
@@ -55,3 +56,81 @@ func TestIsSQLiteReadOnlyError(t *testing.T) {
|
||||
t.Fatalf("expected SQLITE_READONLY to be detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapDiagnosticErrorCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{name: "nil", err: nil, want: ""},
|
||||
{name: "not found", err: os.ErrNotExist, want: "permissions_missing_path"},
|
||||
{name: "readonly", err: syscall.EROFS, want: "permissions_readonly"},
|
||||
{name: "permission denied", err: syscall.EACCES, want: "permissions_write_denied"},
|
||||
{name: "other", err: errors.New("boom"), want: "permissions_write_failed"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := MapDiagnosticErrorCode(tt.err); got != tt.want {
|
||||
t.Fatalf("MapDiagnosticErrorCode() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPathPermissions(t *testing.T) {
|
||||
t.Run("missing path", func(t *testing.T) {
|
||||
result := CheckPathPermissions("/definitely/missing/path", "rw")
|
||||
if result.Exists {
|
||||
t.Fatalf("expected missing path to not exist")
|
||||
}
|
||||
if result.ErrorCode != "permissions_missing_path" {
|
||||
t.Fatalf("expected permissions_missing_path, got %q", result.ErrorCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("writable file", func(t *testing.T) {
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "perm-file-*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp file: %v", err)
|
||||
}
|
||||
if closeErr := tempFile.Close(); closeErr != nil {
|
||||
t.Fatalf("close temp file: %v", closeErr)
|
||||
}
|
||||
|
||||
result := CheckPathPermissions(tempFile.Name(), "rw")
|
||||
if !result.Exists {
|
||||
t.Fatalf("expected file to exist")
|
||||
}
|
||||
if !result.Writable {
|
||||
t.Fatalf("expected file to be writable, got error: %s", result.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("writable directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
result := CheckPathPermissions(dir, "rwx")
|
||||
if !result.Exists {
|
||||
t.Fatalf("expected directory to exist")
|
||||
}
|
||||
if !result.Writable {
|
||||
t.Fatalf("expected directory to be writable, got error: %s", result.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no write required", func(t *testing.T) {
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "perm-read-*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp file: %v", err)
|
||||
}
|
||||
if closeErr := tempFile.Close(); closeErr != nil {
|
||||
t.Fatalf("close temp file: %v", closeErr)
|
||||
}
|
||||
|
||||
result := CheckPathPermissions(tempFile.Name(), "r")
|
||||
if result.Writable {
|
||||
t.Fatalf("expected writable=false when write permission is not required")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user