mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-30 21:29:44 -06:00
252 lines
7.5 KiB
Go
252 lines
7.5 KiB
Go
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
v10Validator "github.com/go-playground/validator/v10"
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
|
"github.com/hatchet-dev/hatchet/pkg/errors"
|
|
|
|
goerrors "errors"
|
|
)
|
|
|
|
const (
|
|
EmailErr = "Invalid email address"
|
|
PasswordErr = "Invalid password. Passwords must be at least 8 characters in length, contain an upper and lowercase letter, and contain at least one number."
|
|
UUIDErr = "Invalid UUID reference"
|
|
HatchetNameErr = "Hatchet names must match the regex ^[a-zA-Z0-9\\.\\-_]+$"
|
|
ActionIDErr = "Invalid action ID. Action IDs must be in the format <integrationId>:<verb>"
|
|
CronErr = "Invalid cron expression"
|
|
DurationErr = "Invalid duration. Durations must be in the format <number><unit>, where unit is one of: 's', 'm', 'h'"
|
|
CELExprErr = "Invalid CEL expression"
|
|
)
|
|
|
|
type APIErrors gen.APIErrors
|
|
|
|
func (a *APIErrors) String() string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("Validation failed with the following errors:\n")
|
|
|
|
for i, err := range a.Errors {
|
|
sb.WriteString(fmt.Sprintf("%d: %s\n", i, err.Description))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// Validator will validate the fields for a request object to ensure that
|
|
// the request is well-formed. For example, it searches for required fields
|
|
// or verifies that fields are of a semantic type (like email)
|
|
type Validator interface {
|
|
// Validate accepts a generic struct for validating. It returns a request
|
|
// error that is meant to be shown to the end user as a readable string.
|
|
Validate(s interface{}) error
|
|
|
|
ValidateAPI(s interface{}) (*APIErrors, error)
|
|
}
|
|
|
|
// DefaultValidator uses the go-playground v10 validator for verifying that
|
|
// request objects are well-formed
|
|
type DefaultValidator struct {
|
|
v10 *v10Validator.Validate
|
|
}
|
|
|
|
// NewDefaultValidator returns a Validator constructed from the go-playground v10
|
|
// validator
|
|
func NewDefaultValidator() Validator {
|
|
return &DefaultValidator{newValidator()}
|
|
}
|
|
|
|
func (v *DefaultValidator) ValidateAPI(s interface{}) (*APIErrors, error) {
|
|
err := v.v10.Struct(s)
|
|
|
|
if err == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// translate all validator errors
|
|
errs, ok := err.(v10Validator.ValidationErrors)
|
|
|
|
if !ok {
|
|
return nil, errors.NewErrInternal(fmt.Errorf("could not cast err to validator.ValidationErrors, type %T", err))
|
|
}
|
|
|
|
// convert all validator errors to error strings
|
|
apiErrors := make([]gen.APIError, len(errs))
|
|
|
|
for i, field := range errs {
|
|
errObj := NewValidationErrObject(field)
|
|
fieldStr := strings.ToLower(errObj.Field)
|
|
|
|
apiErrors[i].Description = getErrorStr(errObj)
|
|
apiErrors[i].Field = &fieldStr
|
|
}
|
|
|
|
return &APIErrors{
|
|
Errors: apiErrors,
|
|
}, nil
|
|
}
|
|
|
|
// Validate uses the go-playground v10 validator and checks struct fields against
|
|
// a `form:"<validator>"` tag.
|
|
func (v *DefaultValidator) Validate(s interface{}) error {
|
|
err := v.v10.Struct(s)
|
|
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// translate all validator errors
|
|
errs, ok := err.(v10Validator.ValidationErrors)
|
|
|
|
if !ok {
|
|
return errors.NewErrInternal(fmt.Errorf("could not cast err to validator.ValidationErrors, type %T", err))
|
|
}
|
|
|
|
// convert all validator errors to error strings
|
|
errorStrs := make([]string, len(errs))
|
|
|
|
for i, field := range errs {
|
|
errObj := NewValidationErrObject(field)
|
|
|
|
errorStrs[i] = getErrorStr(errObj)
|
|
}
|
|
|
|
return NewErrFailedRequestValidation(errorStrs...)
|
|
}
|
|
|
|
func getErrorStr(errObj *ValidationErrObject) string {
|
|
switch strings.ToLower(errObj.Condition) {
|
|
case "password":
|
|
return PasswordErr
|
|
case "email":
|
|
return errObj.SafeExternalError(EmailErr)
|
|
case "hatchetname":
|
|
return errObj.SafeExternalError(HatchetNameErr)
|
|
case "uuid":
|
|
return errObj.SafeExternalError(UUIDErr)
|
|
case "actionid":
|
|
return errObj.SafeExternalError(ActionIDErr)
|
|
case "cron":
|
|
return errObj.SafeExternalError(CronErr)
|
|
case "duration":
|
|
return errObj.SafeExternalError(DurationErr)
|
|
case "celworkflowrunstr":
|
|
return errObj.SafeExternalError(CELExprErr)
|
|
case "celsteprunstr":
|
|
return errObj.SafeExternalError(CELExprErr)
|
|
default:
|
|
return errObj.SafeExternalError("")
|
|
}
|
|
}
|
|
|
|
func NewErrFailedRequestValidation(valErrors ...string) error {
|
|
var err error
|
|
|
|
for _, valErr := range valErrors {
|
|
err = multierror.Append(err, goerrors.New(valErr))
|
|
}
|
|
|
|
return errors.NewError(
|
|
400,
|
|
"Bad Request",
|
|
err.Error(),
|
|
"",
|
|
)
|
|
}
|
|
|
|
// ValidationErrObject represents an error referencing a specific field in a struct that
|
|
// must match a specific condition. This object is modeled off of the go-playground v10
|
|
// validator `FieldError` type, but can be used generically for any request validation
|
|
// issues that occur downstream.
|
|
type ValidationErrObject struct {
|
|
// Field is the request field that has a validation error.
|
|
Field string
|
|
|
|
// Namespace contains a path to the field which has a validation error
|
|
Namespace string
|
|
|
|
// Condition is the condition that was not satisfied, resulting in the validation
|
|
// error
|
|
Condition string
|
|
|
|
// Param is an optional field that shows a parameter that was not satisfied. For example,
|
|
// the field value was not found in the set [ "value1", "value2" ], so "value1", "value2"
|
|
// is the parameter in this case.
|
|
Param string
|
|
|
|
// ActualValue is the actual value of the field that failed validation.
|
|
ActualValue interface{}
|
|
}
|
|
|
|
// NewValidationErrObject simply returns a ValidationErrObject from a go-playground v10
|
|
// validator `FieldError`
|
|
func NewValidationErrObject(fieldErr v10Validator.FieldError) *ValidationErrObject {
|
|
return &ValidationErrObject{
|
|
Field: fieldErr.Field(),
|
|
Namespace: fieldErr.StructNamespace(),
|
|
Condition: fieldErr.ActualTag(),
|
|
Param: fieldErr.Param(),
|
|
ActualValue: fieldErr.Value(),
|
|
}
|
|
}
|
|
|
|
// SafeExternalError converts the ValidationErrObject to a string that is readable and safe
|
|
// to send externally. In this case, "safe" means that when the `ActualValue` field is cast
|
|
// to a string, it is type-checked so that only certain types are passed to the user. We
|
|
// don't want an upstream command accidentally setting a complex object in the request field
|
|
// that could leak sensitive information to the user. To limit this, we only support sending
|
|
// static `ActualValue` types: `string`, `int`, `[]string`, and `[]int`. Otherwise, we say that
|
|
// the actual value is "invalid type".
|
|
//
|
|
// Note: the test cases split on "," to parse out the different errors. Don't add commas to the
|
|
// safe external error.
|
|
func (obj *ValidationErrObject) SafeExternalError(suffix string) string {
|
|
if suffix == "" {
|
|
suffix = fmt.Sprintf("on condition '%s'", obj.Condition)
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("validation failed on field '%s': %s", obj.Namespace, suffix))
|
|
|
|
if obj.Param != "" {
|
|
sb.WriteString(fmt.Sprintf(" [ %s ]: got %s", obj.Param, obj.getActualValueString()))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (obj *ValidationErrObject) getActualValueString() string {
|
|
// we translate to "json-readable" form for nil values, since clients may not be Golang
|
|
if obj.ActualValue == nil {
|
|
return "null"
|
|
}
|
|
|
|
// create type switch statement to make sure that we don't accidentally leak
|
|
// data. we only want to write strings, numbers, or slices of strings/numbers.
|
|
// different data types can be added if necessary, as long as they are checked
|
|
switch v := obj.ActualValue.(type) {
|
|
case int:
|
|
return fmt.Sprintf("%d", v)
|
|
case string:
|
|
return fmt.Sprintf("'%s'", v)
|
|
case []string:
|
|
return fmt.Sprintf("[ %s ]", strings.Join(v, " "))
|
|
case []int:
|
|
strArr := make([]string, len(v))
|
|
|
|
for i, intItem := range v {
|
|
strArr[i] = fmt.Sprintf("%d", intItem)
|
|
}
|
|
|
|
return fmt.Sprintf("[ %s ]", strings.Join(strArr, " "))
|
|
default:
|
|
return "invalid type"
|
|
}
|
|
}
|