Files
Charon/backend/internal/util/permissions.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

176 lines
4.5 KiB
Go

package util
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
)
type PermissionCheck struct {
Path string `json:"path"`
Required string `json:"required"`
Exists bool `json:"exists"`
Writable bool `json:"writable"`
OwnerUID int `json:"owner_uid"`
OwnerGID int `json:"owner_gid"`
Mode string `json:"mode"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
func CheckPathPermissions(path, required string) PermissionCheck {
result := PermissionCheck{
Path: path,
Required: required,
}
if strings.ContainsRune(path, '\x00') {
result.Writable = false
result.Error = "invalid path"
result.ErrorCode = "permissions_invalid_path"
return result
}
cleanPath := filepath.Clean(path)
linkInfo, linkErr := os.Lstat(cleanPath)
if linkErr != nil {
result.Writable = false
result.Error = linkErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(linkErr)
return result
}
if linkInfo.Mode()&os.ModeSymlink != 0 {
result.Writable = false
result.Error = "symlink paths are not supported"
result.ErrorCode = "permissions_unsupported_type"
return result
}
info, err := os.Stat(cleanPath)
if err != nil {
result.Writable = false
result.Error = err.Error()
result.ErrorCode = MapDiagnosticErrorCode(err)
return result
}
result.Exists = true
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
result.OwnerUID = int(stat.Uid)
result.OwnerGID = int(stat.Gid)
}
result.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
if !info.IsDir() && !info.Mode().IsRegular() {
result.Writable = false
result.Error = "unsupported file type"
result.ErrorCode = "permissions_unsupported_type"
return result
}
if strings.Contains(required, "w") {
if info.IsDir() {
probeFile, probeErr := os.CreateTemp(cleanPath, "permcheck-*")
if probeErr != nil {
result.Writable = false
result.Error = probeErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(probeErr)
return result
}
if closeErr := probeFile.Close(); closeErr != nil {
result.Writable = false
result.Error = closeErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(closeErr)
return result
}
if removeErr := os.Remove(probeFile.Name()); removeErr != nil {
result.Writable = false
result.Error = removeErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(removeErr)
return result
}
result.Writable = true
return result
}
file, openErr := os.OpenFile(cleanPath, os.O_WRONLY, 0) // #nosec G304 -- cleanPath is normalized, existence-checked, non-symlink, and regular-file validated above.
if openErr != nil {
result.Writable = false
result.Error = openErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(openErr)
return result
}
if closeErr := file.Close(); closeErr != nil {
result.Writable = false
result.Error = closeErr.Error()
result.ErrorCode = MapDiagnosticErrorCode(closeErr)
return result
}
result.Writable = true
return result
}
result.Writable = false
return result
}
func MapDiagnosticErrorCode(err error) string {
switch {
case err == nil:
return ""
case os.IsNotExist(err):
return "permissions_missing_path"
case errors.Is(err, syscall.EROFS):
return "permissions_readonly"
case errors.Is(err, syscall.EACCES) || os.IsPermission(err):
return "permissions_write_denied"
default:
return "permissions_write_failed"
}
}
func MapSaveErrorCode(err error) (string, bool) {
switch {
case err == nil:
return "", false
case IsSQLiteReadOnlyError(err):
return "permissions_db_readonly", true
case IsSQLiteLockedError(err):
return "permissions_db_locked", true
case errors.Is(err, syscall.EROFS):
return "permissions_readonly", true
case errors.Is(err, syscall.EACCES) || os.IsPermission(err):
return "permissions_write_denied", true
case strings.Contains(strings.ToLower(err.Error()), "permission denied"):
return "permissions_write_denied", true
default:
return "", false
}
}
func IsSQLiteReadOnlyError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "readonly") ||
strings.Contains(msg, "read-only") ||
strings.Contains(msg, "attempt to write a readonly database") ||
strings.Contains(msg, "sqlite_readonly")
}
func IsSQLiteLockedError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "database is locked") ||
strings.Contains(msg, "sqlite_busy") ||
strings.Contains(msg, "database locked")
}