Files
skybridge/internal/middleware/validation.go
2025-08-22 14:40:59 -04:00

266 lines
6.9 KiB
Go

package middleware
import (
"net/http"
"reflect"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
// ValidationError represents a validation error
type ValidationError struct {
Field string `json:"field"`
Tag string `json:"tag"`
Value string `json:"value"`
Message string `json:"message"`
}
// ValidationResponse represents the validation error response
type ValidationResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Details []ValidationError `json:"details,omitempty"`
}
var validate *validator.Validate
func init() {
validate = validator.New()
// Register custom tag name function to use json tags
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
}
// ValidateJSON validates JSON request body against struct validation tags
func ValidateJSON(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Skip validation for GET requests and requests without body
if c.Request.Method == "GET" || c.Request.ContentLength == 0 {
c.Next()
return
}
// Store original body for potential re-reading
c.Set("validation_enabled", true)
c.Next()
}
}
// ValidateStruct validates a struct and returns formatted errors
func ValidateStruct(s interface{}) []ValidationError {
var errors []ValidationError
err := validate.Struct(s)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
var element ValidationError
element.Field = err.Field()
element.Tag = err.Tag()
element.Value = err.Param()
element.Message = getErrorMessage(err)
errors = append(errors, element)
}
}
return errors
}
// ValidateAndBind validates and binds JSON request to struct
func ValidateAndBind(c *gin.Context, obj interface{}) error {
// Bind JSON to struct
if err := c.ShouldBindJSON(obj); err != nil {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Invalid JSON",
Message: "Request body contains invalid JSON: " + err.Error(),
})
return err
}
// Validate struct
if validationErrors := ValidateStruct(obj); len(validationErrors) > 0 {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Validation Failed",
Message: "Request validation failed",
Details: validationErrors,
})
return validator.ValidationErrors{}
}
return nil
}
// getErrorMessage returns a human-readable error message for validation errors
func getErrorMessage(fe validator.FieldError) string {
switch fe.Tag() {
case "required":
return "This field is required"
case "email":
return "Invalid email format"
case "min":
return "Value is too short (minimum " + fe.Param() + " characters)"
case "max":
return "Value is too long (maximum " + fe.Param() + " characters)"
case "url":
return "Invalid URL format"
case "oneof":
return "Value must be one of: " + fe.Param()
case "uuid":
return "Invalid UUID format"
case "gte":
return "Value must be greater than or equal to " + fe.Param()
case "lte":
return "Value must be less than or equal to " + fe.Param()
case "len":
return "Value must be exactly " + fe.Param() + " characters"
case "dive":
return "Invalid array element"
default:
return "Invalid value for " + fe.Field()
}
}
// RequiredFields validates that specific fields are present in the request
func RequiredFields(fields ...string) gin.HandlerFunc {
return func(c *gin.Context) {
var json map[string]interface{}
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Invalid JSON",
Message: "Request body contains invalid JSON",
})
c.Abort()
return
}
var missingFields []string
for _, field := range fields {
if _, exists := json[field]; !exists {
missingFields = append(missingFields, field)
}
}
if len(missingFields) > 0 {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Missing Required Fields",
Message: "The following required fields are missing: " + strings.Join(missingFields, ", "),
})
c.Abort()
return
}
// Store the parsed JSON for use in handlers
c.Set("parsed_json", json)
c.Next()
}
}
// ValidateUUID validates that a URL parameter is a valid UUID
func ValidateUUID(param string) gin.HandlerFunc {
return func(c *gin.Context) {
value := c.Param(param)
if value == "" {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Missing Parameter",
Message: "Required parameter '" + param + "' is missing",
})
c.Abort()
return
}
// Validate UUID format
if err := validate.Var(value, "uuid"); err != nil {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Invalid Parameter",
Message: "Parameter '" + param + "' must be a valid UUID",
})
c.Abort()
return
}
c.Next()
}
}
// ValidateQueryParams validates query parameters
func ValidateQueryParams(rules map[string]string) gin.HandlerFunc {
return func(c *gin.Context) {
var errors []ValidationError
for param, rule := range rules {
value := c.Query(param)
if value != "" {
if err := validate.Var(value, rule); err != nil {
for _, err := range err.(validator.ValidationErrors) {
errors = append(errors, ValidationError{
Field: param,
Tag: err.Tag(),
Value: err.Param(),
Message: getErrorMessage(err),
})
}
}
}
}
if len(errors) > 0 {
c.JSON(http.StatusBadRequest, ValidationResponse{
Error: "Invalid Query Parameters",
Message: "One or more query parameters are invalid",
Details: errors,
})
c.Abort()
return
}
c.Next()
}
}
// SanitizeInput sanitizes input strings to prevent XSS and injection attacks
func SanitizeInput() gin.HandlerFunc {
return func(c *gin.Context) {
// This is a basic implementation - in production you might want to use
// a more sophisticated sanitization library like bluemonday
c.Next()
}
}
// ValidatePermissions validates that permission scopes follow the expected format
func ValidatePermissions(c *gin.Context, permissions []string) []ValidationError {
var errors []ValidationError
for i, perm := range permissions {
// Check basic format: should contain only alphanumeric, dots, and underscores
if err := validate.Var(perm, "required,min=1,max=255,alphanum|contains=.|contains=_"); err != nil {
errors = append(errors, ValidationError{
Field: "permissions[" + string(rune(i)) + "]",
Tag: "format",
Value: perm,
Message: "Permission scope must contain only alphanumeric characters, dots, and underscores",
})
}
// Check for dangerous patterns
if strings.Contains(perm, "..") || strings.HasPrefix(perm, ".") || strings.HasSuffix(perm, ".") {
errors = append(errors, ValidationError{
Field: "permissions[" + string(rune(i)) + "]",
Tag: "format",
Value: perm,
Message: "Permission scope has invalid format",
})
}
}
return errors
}