validator.go 5.0 KB

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