- Added role-based middleware to various security handler tests to ensure only admin users can access certain endpoints. - Created a new test file for authorization checks on security mutators, verifying that non-admin users receive forbidden responses. - Updated existing tests to include role setting for admin users, ensuring consistent access control during testing. - Introduced sensitive data masking in settings handler responses, ensuring sensitive values are not exposed in API responses. - Enhanced user handler responses to mask API keys and invite tokens, providing additional security for user-related endpoints. - Refactored routes to group security admin endpoints under a dedicated route with role-based access control. - Added tests for import handler routes to verify authorization guards, ensuring only admin users can access import functionalities.
122 lines
2.7 KiB
Go
122 lines
2.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
"github.com/Wikid82/charon/backend/internal/util"
|
|
)
|
|
|
|
func requireAdmin(c *gin.Context) bool {
|
|
if isAdmin(c) {
|
|
return true
|
|
}
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "admin privileges required",
|
|
"error_code": "permissions_admin_only",
|
|
})
|
|
return false
|
|
}
|
|
|
|
func requireAuthenticatedAdmin(c *gin.Context) bool {
|
|
if _, exists := c.Get("userID"); !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
"error": "Authorization header required",
|
|
})
|
|
return false
|
|
}
|
|
|
|
return requireAdmin(c)
|
|
}
|
|
|
|
func isAdmin(c *gin.Context) bool {
|
|
role, _ := c.Get("role")
|
|
roleStr, _ := role.(string)
|
|
return roleStr == "admin"
|
|
}
|
|
|
|
func respondPermissionError(c *gin.Context, securityService *services.SecurityService, action string, err error, path string) bool {
|
|
code, ok := util.MapSaveErrorCode(err)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
admin := isAdmin(c)
|
|
response := gin.H{
|
|
"error": permissionErrorMessage(code),
|
|
"error_code": code,
|
|
}
|
|
|
|
if admin {
|
|
if path != "" {
|
|
response["path"] = path
|
|
}
|
|
response["help"] = buildPermissionHelp(path)
|
|
} else {
|
|
response["help"] = "Check volume permissions or contact an administrator."
|
|
}
|
|
|
|
logPermissionAudit(securityService, c, action, code, path, admin)
|
|
c.JSON(http.StatusInternalServerError, response)
|
|
return true
|
|
}
|
|
|
|
func permissionErrorMessage(code string) string {
|
|
switch code {
|
|
case "permissions_db_readonly":
|
|
return "database is read-only"
|
|
case "permissions_db_locked":
|
|
return "database is locked"
|
|
case "permissions_readonly":
|
|
return "filesystem is read-only"
|
|
case "permissions_write_denied":
|
|
return "permission denied"
|
|
default:
|
|
return "permission error"
|
|
}
|
|
}
|
|
|
|
func buildPermissionHelp(path string) string {
|
|
uid := os.Geteuid()
|
|
gid := os.Getegid()
|
|
if path == "" {
|
|
return fmt.Sprintf("chown -R %d:%d <path-to-volume>", uid, gid)
|
|
}
|
|
return fmt.Sprintf("chown -R %d:%d %s", uid, gid, path)
|
|
}
|
|
|
|
func logPermissionAudit(securityService *services.SecurityService, c *gin.Context, action, code, path string, admin bool) {
|
|
if securityService == nil {
|
|
return
|
|
}
|
|
|
|
details := map[string]any{
|
|
"error_code": code,
|
|
"admin": admin,
|
|
}
|
|
if admin && path != "" {
|
|
details["path"] = path
|
|
}
|
|
detailsJSON, _ := json.Marshal(details)
|
|
|
|
actor := "unknown"
|
|
if userID, ok := c.Get("userID"); ok {
|
|
actor = fmt.Sprintf("%v", userID)
|
|
}
|
|
|
|
_ = securityService.LogAudit(&models.SecurityAudit{
|
|
Actor: actor,
|
|
Action: action,
|
|
EventCategory: "permissions",
|
|
Details: string(detailsJSON),
|
|
IPAddress: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
})
|
|
}
|