-
Notifications
You must be signed in to change notification settings - Fork 145
/
default.go
227 lines (184 loc) · 6.88 KB
/
default.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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"
)
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"
)
// 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{}) (*gen.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{}) (*gen.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 &gen.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)
default:
return errObj.SafeExternalError("")
}
}
func NewErrFailedRequestValidation(valErrors ...string) error {
var err error
for _, valErr := range valErrors {
err = multierror.Append(err, fmt.Errorf(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"
}
}