Chore: Imperative request data binding (#39837)

* rename Bind to BindMiddleware

* make things private

* removed unused part of data bindings

* provide json and form binding helpers

* add example of binding migration in login api

* implement validation

* fix tests

* remove debug output

* put new bind api into macaron pacakge

* revert bind api breaking change
This commit is contained in:
Serge Zaitsev 2021-10-06 12:52:27 +02:00 committed by GitHub
parent 7fd7c98540
commit 3131388084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 225 additions and 129 deletions

View File

@ -40,7 +40,7 @@ func (hs *HTTPServer) registerRoutes() {
// not logged in views
r.Get("/logout", hs.Logout)
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), routing.Wrap(hs.LoginPost))
r.Post("/login", quota("session"), routing.Wrap(hs.LoginPost))
r.Get("/login/:name", quota("session"), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)

View File

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
macaron "gopkg.in/macaron.v1"
)
const (
@ -170,7 +171,11 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response {
return response.Error(401, "Unauthorized", nil)
}
func (hs *HTTPServer) LoginPost(c *models.ReqContext, cmd dtos.LoginCommand) response.Response {
func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response {
cmd := dtos.LoginCommand{}
if err := macaron.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad login data", err)
}
authModule := ""
var user *models.User
var resp *response.NormalResponse

View File

@ -1,9 +1,11 @@
package api
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -336,11 +338,9 @@ func TestLoginPostRedirect(t *testing.T) {
hs.Cfg.CookieSecure = true
sc.defaultHandler = routing.Wrap(func(w http.ResponseWriter, c *models.ReqContext) response.Response {
cmd := dtos.LoginCommand{
User: "admin",
Password: "admin",
}
return hs.LoginPost(c, cmd)
c.Req.Header.Set("Content-Type", "application/json")
c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`))
return hs.LoginPost(c)
})
bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error {
@ -614,11 +614,10 @@ func TestLoginPostRunLokingHook(t *testing.T) {
}
sc.defaultHandler = routing.Wrap(func(w http.ResponseWriter, c *models.ReqContext) response.Response {
cmd := dtos.LoginCommand{
User: "admin",
Password: "admin",
}
return hs.LoginPost(c, cmd)
c.Req.Header.Set("Content-Type", "application/json")
c.Req.Body = io.NopCloser(bytes.NewBufferString(`{"user":"admin","password":"admin"}`))
x := hs.LoginPost(c)
return x
})
testHook := loginHookTest{}

74
pkg/macaron/binding.go Normal file
View File

@ -0,0 +1,74 @@
package macaron
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
)
// Bind deserializes JSON payload from the request
func Bind(req *http.Request, v interface{}) error {
if req.Body != nil {
defer req.Body.Close()
err := json.NewDecoder(req.Body).Decode(v)
if err != nil && err != io.EOF {
return err
}
}
return validate(v)
}
type Validator interface {
Validate() error
}
func validate(obj interface{}) error {
// If type has a Validate() method - use that
if validator, ok := obj.(Validator); ok {
return validator.Validate()
}
// Otherwise, use relfection to match `binding:"Required"` struct field tags.
// Resolve all pointers and interfaces, until we get a concrete type.
t := reflect.TypeOf(obj)
v := reflect.ValueOf(obj)
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
switch v.Kind() {
// For arrays and slices - iterate over each element and validate it recursively
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
e := v.Index(i).Interface()
if err := validate(e); err != nil {
return err
}
}
// For structs - iterate over each field, check for the "Required" constraint (Macaron legacy), then validate it recursively
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
rule := field.Tag.Get("binding")
if !value.CanInterface() {
continue
}
if rule == "Required" {
zero := reflect.Zero(field.Type).Interface()
if value.Kind() == reflect.Slice {
if value.Len() == 0 {
return fmt.Errorf("required slice %s must not be empty", field.Name)
}
} else if reflect.DeepEqual(zero, value.Interface()) {
return fmt.Errorf("required value %s must not be empty", field.Name)
}
}
if err := validate(value.Interface()); err != nil {
return err
}
}
}
return nil
}

View File

@ -37,11 +37,11 @@ func bind(ctx *macaron.Context, obj interface{}, ifacePtr ...interface{}) {
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
switch {
case strings.Contains(contentType, "form-urlencoded"):
_, _ = ctx.Invoke(Form(obj, ifacePtr...))
_, _ = ctx.Invoke(bindForm(obj, ifacePtr...))
case strings.Contains(contentType, "multipart/form-data"):
_, _ = ctx.Invoke(MultipartForm(obj, ifacePtr...))
_, _ = ctx.Invoke(bindMultipartForm(obj, ifacePtr...))
case strings.Contains(contentType, "json"):
_, _ = ctx.Invoke(Json(obj, ifacePtr...))
_, _ = ctx.Invoke(bindJson(obj, ifacePtr...))
default:
var errors Errors
if contentType == "" {
@ -53,7 +53,7 @@ func bind(ctx *macaron.Context, obj interface{}, ifacePtr ...interface{}) {
ctx.Map(obj) // Map a fake struct so handler won't panic.
}
} else {
_, _ = ctx.Invoke(Form(obj, ifacePtr...))
_, _ = ctx.Invoke(bindForm(obj, ifacePtr...))
}
}
@ -87,9 +87,6 @@ func errorHandler(errs Errors, rw http.ResponseWriter) {
}
}
// CustomErrorHandler will be invoked if errors occured.
var CustomErrorHandler func(*macaron.Context, Errors)
// Bind wraps up the functionality of the Form and Json middleware
// according to the Content-Type and verb of the request.
// A Content-Type is required for POST and PUT requests.
@ -101,26 +98,11 @@ var CustomErrorHandler func(*macaron.Context, Errors)
func Bind(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
bind(ctx, obj, ifacePtr...)
if handler, ok := obj.(ErrorHandler); ok {
_, _ = ctx.Invoke(handler.Error)
} else if CustomErrorHandler != nil {
_, _ = ctx.Invoke(CustomErrorHandler)
} else {
_, _ = ctx.Invoke(errorHandler)
}
_, _ = ctx.Invoke(errorHandler)
}
}
// BindIgnErr will do the exactly same thing as Bind but without any
// error handling, which user has freedom to deal with them.
// This allows user take advantages of validation.
func BindIgnErr(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
bind(ctx, obj, ifacePtr...)
}
}
// Form is middleware to deserialize form-urlencoded data from the request.
// bindForm is middleware to deserialize form-urlencoded data from the request.
// It gets data from the form-urlencoded body, if present, or from the
// query string. It uses the http.Request.ParseForm() method
// to perform deserialization, then reflection is used to map each field
@ -129,7 +111,7 @@ func BindIgnErr(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
// keys, for example: key=val1&key=val2&key=val3
// An interface pointer can be added as a second argument in order
// to map the struct to a specific interface.
func Form(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
func bindForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
var errors Errors
@ -153,11 +135,11 @@ func Form(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
// Set this to whatever value you prefer; default is 10 MB.
var MaxMemory = int64(1024 * 1024 * 10)
// MultipartForm works much like Form, except it can parse multipart forms
// bindMultipartForm works much like Form, except it can parse multipart forms
// and handle file uploads. Like the other deserialization middleware handlers,
// you can pass in an interface to make the interface available for injection
// into other handlers later.
func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
func bindMultipartForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
var errors Errors
ensureNotPointer(formStruct)
@ -189,12 +171,12 @@ func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Hand
}
}
// Json is middleware to deserialize a JSON payload from the request
// bindJson is middleware to deserialize a JSON payload from the request
// into the struct that is passed in. The resulting struct is then
// validated, but no error handling is actually performed here.
// An interface pointer can be added as a second argument in order
// to map the struct to a specific interface.
func Json(jsonStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
func bindJson(jsonStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
var errors Errors
ensureNotPointer(jsonStruct)
@ -210,52 +192,11 @@ func Json(jsonStruct interface{}, ifacePtr ...interface{}) macaron.Handler {
}
}
// URL is the middleware to parse URL parameters into struct fields.
func URL(obj interface{}, ifacePtr ...interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
var errors Errors
ensureNotPointer(obj)
obj := reflect.New(reflect.TypeOf(obj))
val := obj.Elem()
for k, v := range macaron.Params(ctx.Req) {
field := val.FieldByName(k[1:])
if field.IsValid() {
errors = setWithProperType(field.Kind(), v, field, k, errors)
}
}
validateAndMap(obj, ctx, errors, ifacePtr...)
}
}
// RawValidate is same as Validate but does not require a HTTP context,
// and can be used independently just for validation.
// This function does not support Validator interface.
func RawValidate(obj interface{}) Errors {
var errs Errors
v := reflect.ValueOf(obj)
k := v.Kind()
if k == reflect.Interface || k == reflect.Ptr {
v = v.Elem()
k = v.Kind()
}
if k == reflect.Slice || k == reflect.Array {
for i := 0; i < v.Len(); i++ {
e := v.Index(i).Interface()
errs = validateStruct(errs, e)
}
} else {
errs = validateStruct(errs, obj)
}
return errs
}
// Validate is middleware to enforce required fields. If the struct
// passed in implements Validator, then the user-defined Validate method
// validateMiddleware is middleware to enforce required fields. If the struct
// passed in implements Validator, then the user-defined validateMiddleware method
// is executed, and its errors are mapped to the context. This middleware
// performs no error handling: it merely detects errors and maps them.
func Validate(obj interface{}) macaron.Handler {
func validateMiddleware(obj interface{}) macaron.Handler {
return func(ctx *macaron.Context) {
var errs Errors
v := reflect.ValueOf(obj)
@ -268,13 +209,13 @@ func Validate(obj interface{}) macaron.Handler {
for i := 0; i < v.Len(); i++ {
e := v.Index(i).Interface()
errs = validateStruct(errs, e)
if validator, ok := e.(Validator); ok {
if validator, ok := e.(_Validator); ok {
errs = validator.Validate(ctx, errs)
}
}
} else {
errs = validateStruct(errs, obj)
if validator, ok := obj.(Validator); ok {
if validator, ok := obj.(_Validator); ok {
errs = validator.Validate(ctx, errs)
}
}
@ -283,9 +224,9 @@ func Validate(obj interface{}) macaron.Handler {
}
var (
AlphaDashPattern = regexp.MustCompile(`[^\d\w-_]`)
AlphaDashDotPattern = regexp.MustCompile(`[^\d\w-_\.]`)
EmailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?")
alphaDashPattern = regexp.MustCompile(`[^\d\w-_]`)
alphaDashDotPattern = regexp.MustCompile(`[^\d\w-_\.]`)
emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?")
)
// Copied from github.com/asaskevich/govalidator.
@ -323,40 +264,30 @@ func isURL(str string) bool {
}
type (
// Rule represents a validation rule.
Rule struct {
// rule represents a validation rule.
rule struct {
// IsMatch checks if rule matches.
IsMatch func(string) bool
// IsValid applies validation rule to condition.
IsValid func(Errors, string, interface{}) (bool, Errors)
}
// ParamRule does same thing as Rule but passes rule itself to IsValid method.
ParamRule struct {
// paramRule does same thing as Rule but passes rule itself to IsValid method.
paramRule struct {
// IsMatch checks if rule matches.
IsMatch func(string) bool
// IsValid applies validation rule to condition.
IsValid func(Errors, string, string, interface{}) (bool, Errors)
}
// RuleMapper and ParamRuleMapper represent validation rule mappers,
// _RuleMapper and ParamRuleMapper represent validation rule mappers,
// it allwos users to add custom validation rules.
RuleMapper []*Rule
ParamRuleMapper []*ParamRule
_RuleMapper []*rule
_ParamRuleMapper []*paramRule
)
var ruleMapper RuleMapper
var paramRuleMapper ParamRuleMapper
// AddRule adds new validation rule.
func AddRule(r *Rule) {
ruleMapper = append(ruleMapper, r)
}
// AddParamRule adds new validation rule.
func AddParamRule(r *ParamRule) {
paramRuleMapper = append(paramRuleMapper, r)
}
var ruleMapper _RuleMapper
var paramRuleMapper _ParamRuleMapper
func in(fieldValue interface{}, arr string) bool {
val := fmt.Sprintf("%v", fieldValue)
@ -464,12 +395,12 @@ VALIDATE_RULES:
break VALIDATE_RULES
}
case rule == "AlphaDash":
if AlphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
if alphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
errors.Add([]string{field.Name}, ERR_ALPHA_DASH, "AlphaDash")
break VALIDATE_RULES
}
case rule == "AlphaDashDot":
if AlphaDashDotPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
if alphaDashDotPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
errors.Add([]string{field.Name}, ERR_ALPHA_DASH_DOT, "AlphaDashDot")
break VALIDATE_RULES
}
@ -525,7 +456,7 @@ VALIDATE_RULES:
break VALIDATE_RULES
}
case rule == "Email":
if !EmailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
if !emailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
errors.Add([]string{field.Name}, ERR_EMAIL, "Email")
break VALIDATE_RULES
}
@ -590,9 +521,6 @@ VALIDATE_RULES:
return errors
}
// NameMapper represents a form tag name mapper.
type NameMapper func(string) string
var (
nameMapper = func(field string) string {
newstr := make([]rune, 0, len(field))
@ -609,11 +537,6 @@ var (
}
)
// SetNameMapper sets name mapper.
func SetNameMapper(nm NameMapper) {
nameMapper = nm
}
// Takes values from the form data and puts them into a struct
func mapForm(formStruct reflect.Value, form map[string][]string,
formfile map[string][]*multipart.FileHeader, errors Errors) Errors {
@ -755,7 +678,7 @@ func ensureNotPointer(obj interface{}) {
// with errors from deserialization, then maps both the
// resulting struct and the errors to the context.
func validateAndMap(obj reflect.Value, ctx *macaron.Context, errors Errors, ifacePtr ...interface{}) {
_, _ = ctx.Invoke(Validate(obj.Interface()))
_, _ = ctx.Invoke(validateMiddleware(obj.Interface()))
errors = append(errors, getErrors(ctx)...)
ctx.Map(errors)
ctx.Map(obj.Elem().Interface())
@ -770,15 +693,9 @@ func getErrors(ctx *macaron.Context) Errors {
}
type (
// ErrorHandler is the interface that has custom error handling process.
ErrorHandler interface {
// Error handles validation errors with custom process.
Error(*macaron.Context, Errors)
}
// Validator is the interface that handles some rudimentary
// _Validator is the interface that handles some rudimentary
// request validation logic so your application doesn't have to.
Validator interface {
_Validator interface {
// Validate validates that the request is OK. It is recommended
// that validation be limited to checking values for syntax and
// semantics, enough to know that you can make sense of the request

101
pkg/macaron/binding_test.go Normal file
View File

@ -0,0 +1,101 @@
package macaron
import (
"errors"
"testing"
)
type StructWithInt struct {
A int `binding:"Required"`
}
type StructWithPrimitives struct {
A int `binding:"Required"`
B string `binding:"Required"`
C bool `binding:"Required"`
D float64 `binding:"Required"`
}
type StructWithPrivateFields struct {
A int `binding:"Required"` // must be validated
b int `binding:"Required"` // will not be used
}
type StructWithInterface struct {
A interface{} `binding:"Required"`
}
type StructWithSliceInts struct {
A []int `binding:"Required"`
}
type StructWithSliceStructs struct {
A []StructWithInt `binding:"Required"`
}
type StructWithSliceInterfaces struct {
A []interface{} `binding:"Required"`
}
type StructWithStruct struct {
A StructWithInt `binding:"Required"`
}
type StructWithStructPointer struct {
A *StructWithInt `binding:"Required"`
}
type StructWithValidation struct {
A int
}
func (sv StructWithValidation) Validate() error {
if sv.A < 10 {
return errors.New("too small")
}
return nil
}
func TestValidationSuccess(t *testing.T) {
for _, x := range []interface{}{
42,
"foo",
struct{ A int }{},
StructWithInt{42},
StructWithPrimitives{42, "foo", true, 12.34},
StructWithPrivateFields{12, 0},
StructWithInterface{"foo"},
StructWithSliceInts{[]int{1, 2, 3}},
StructWithSliceInterfaces{[]interface{}{1, 2, 3}},
StructWithSliceStructs{[]StructWithInt{{1}, {2}}},
StructWithStruct{StructWithInt{3}},
StructWithStructPointer{&StructWithInt{3}},
StructWithValidation{42},
} {
if err := validate(x); err != nil {
t.Error("Validation failed:", x, err)
}
}
}
func TestValidationFailure(t *testing.T) {
for i, x := range []interface{}{
StructWithInt{0},
StructWithPrimitives{0, "foo", true, 12.34},
StructWithPrimitives{42, "", true, 12.34},
StructWithPrimitives{42, "foo", false, 12.34},
StructWithPrimitives{42, "foo", true, 0},
StructWithPrivateFields{0, 1},
StructWithInterface{},
StructWithInterface{nil},
StructWithSliceInts{},
StructWithSliceInts{[]int{}},
StructWithSliceStructs{[]StructWithInt{}},
StructWithSliceStructs{[]StructWithInt{{0}, {2}}},
StructWithSliceStructs{[]StructWithInt{{2}, {0}}},
StructWithSliceInterfaces{[]interface{}{}},
StructWithSliceInterfaces{nil},
StructWithStruct{StructWithInt{}},
StructWithStruct{StructWithInt{0}},
StructWithStructPointer{},
StructWithStructPointer{&StructWithInt{}},
StructWithValidation{2},
} {
if err := validate(x); err == nil {
t.Error("Validation should fail:", i, x)
}
}
}