validator.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. package requestutils
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "github.com/porter-dev/porter/api/server/shared/apierrors"
  7. "github.com/go-playground/validator/v10"
  8. )
  9. // Validator will validate the fields for a request object to ensure that
  10. // the request is well-formed. For example, it searches for required fields
  11. // or verifies that fields are of a semantic type (like email)
  12. type Validator interface {
  13. // Validate accepts a generic struct for validating. It returns a request
  14. // error that is meant to be shown to the end user as a readable string.
  15. Validate(s interface{}) apierrors.RequestError
  16. }
  17. // DefaultValidator uses the go-playground v10 validator for verifying that
  18. // request objects are well-formed
  19. type DefaultValidator struct {
  20. v10 *validator.Validate
  21. }
  22. // NewDefaultValidator returns a Validator constructed from the go-playground v10
  23. // validator
  24. func NewDefaultValidator() Validator {
  25. v10 := validator.New()
  26. // set tag name to "form" since the request structs are used on both
  27. // the client and server side
  28. v10.SetTagName("form")
  29. return &DefaultValidator{v10}
  30. }
  31. // Validate uses the go-playground v10 validator and checks struct fields against
  32. // a `form:"<validator>"` tag.
  33. func (v *DefaultValidator) Validate(s interface{}) apierrors.RequestError {
  34. err := v.v10.Struct(s)
  35. if err == nil {
  36. return nil
  37. }
  38. // translate all validator errors
  39. errs, ok := err.(validator.ValidationErrors)
  40. if !ok {
  41. return apierrors.NewErrInternal(fmt.Errorf("could not cast err to validator.ValidationErrors"))
  42. }
  43. // convert all validator errors to error strings
  44. errorStrs := make([]string, len(errs))
  45. for i, field := range errs {
  46. errObj := NewValidationErrObject(field)
  47. errorStrs[i] = errObj.SafeExternalError()
  48. }
  49. return NewErrFailedRequestValidation(strings.Join(errorStrs, ","))
  50. }
  51. func NewErrFailedRequestValidation(valError string) apierrors.RequestError {
  52. // return 400 error since a validation error indicates an issue with the user request
  53. return apierrors.NewErrPassThroughToClient(fmt.Errorf(valError), http.StatusBadRequest)
  54. }
  55. // ValidationErrObject represents an error referencing a specific field in a struct that
  56. // must match a specific condition. This object is modeled off of the go-playground v10
  57. // validator `FieldError` type, but can be used generically for any request validation
  58. // issues that occur downstream.
  59. type ValidationErrObject struct {
  60. // Field is the request field that has a validation error.
  61. Field string
  62. // Condition is the condition that was not satisfied, resulting in the validation
  63. // error
  64. Condition string
  65. // Param is an optional field that shows a parameter that was not satisfied. For example,
  66. // the field value was not found in the set [ "value1", "value2" ], so "value1", "value2"
  67. // is the parameter in this case.
  68. Param string
  69. // ActualValue is the actual value of the field that failed validation.
  70. ActualValue interface{}
  71. }
  72. // NewValidationErrObject simply returns a ValidationErrObject from a go-playground v10
  73. // validator `FieldError`
  74. func NewValidationErrObject(fieldErr validator.FieldError) *ValidationErrObject {
  75. return &ValidationErrObject{
  76. Field: fieldErr.Field(),
  77. Condition: fieldErr.ActualTag(),
  78. Param: fieldErr.Param(),
  79. ActualValue: fieldErr.Value(),
  80. }
  81. }
  82. // SafeExternalError converts the ValidationErrObject to a string that is readable and safe
  83. // to send externally. In this case, "safe" means that when the `ActualValue` field is cast
  84. // to a string, it is type-checked so that only certain types are passed to the user. We
  85. // don't want an upstream command accidentally setting a complex object in the request field
  86. // that could leak sensitive information to the user. To limit this, we only support sending
  87. // static `ActualValue` types: `string`, `int`, `[]string`, and `[]int`. Otherwise, we say that
  88. // the actual value is "invalid type".
  89. //
  90. // Note: the test cases split on "," to parse out the different errors. Don't add commas to the
  91. // safe external error.
  92. func (obj *ValidationErrObject) SafeExternalError() string {
  93. var sb strings.Builder
  94. sb.WriteString(fmt.Sprintf("validation failed on field '%s' on condition '%s'", obj.Field, obj.Condition))
  95. if obj.Param != "" {
  96. sb.WriteString(fmt.Sprintf(" [ %s ]: got %s", obj.Param, obj.getActualValueString()))
  97. }
  98. return sb.String()
  99. }
  100. func (obj *ValidationErrObject) getActualValueString() string {
  101. // we translate to "json-readable" form for nil values, since clients may not be Golang
  102. if obj.ActualValue == nil {
  103. return "null"
  104. }
  105. // create type switch statement to make sure that we don't accidentally leak
  106. // data. we only want to write strings, numbers, or slices of strings/numbers.
  107. // different data types can be added if necessary, as long as they are checked
  108. switch v := obj.ActualValue.(type) {
  109. case int:
  110. return fmt.Sprintf("%d", v)
  111. case string:
  112. return fmt.Sprintf("'%s'", v)
  113. case []string:
  114. return fmt.Sprintf("[ %s ]", strings.Join(v, " "))
  115. case []int:
  116. strArr := make([]string, len(v))
  117. for i, intItem := range v {
  118. strArr[i] = fmt.Sprintf("%d", intItem)
  119. }
  120. return fmt.Sprintf("[ %s ]", strings.Join(strArr, " "))
  121. default:
  122. return "invalid type"
  123. }
  124. }