2021-07-16 18:28:13 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
2021-08-03 19:56:54 +00:00
|
|
|
"bufio"
|
2021-07-16 18:28:13 +00:00
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2021-08-03 19:56:54 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2021-07-16 18:28:13 +00:00
|
|
|
"net/http"
|
|
|
|
"reflect"
|
2021-08-03 19:56:54 +00:00
|
|
|
"strings"
|
2021-07-16 18:28:13 +00:00
|
|
|
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
|
|
"github.com/go-kit/kit/endpoint"
|
2021-08-03 19:56:54 +00:00
|
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
2021-08-03 13:33:27 +00:00
|
|
|
"github.com/pkg/errors"
|
2021-07-16 18:28:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type handlerFunc func(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error)
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
// parseTag parses a `url` tag and whether it's optional or not, which is an optional part of the tag
|
|
|
|
func parseTag(tag string) (string, bool, error) {
|
|
|
|
parts := strings.Split(tag, ",")
|
|
|
|
switch len(parts) {
|
|
|
|
case 0:
|
|
|
|
return "", false, errors.Errorf("Error parsing %s: too few parts", tag)
|
|
|
|
case 1:
|
|
|
|
return tag, false, nil
|
|
|
|
case 2:
|
|
|
|
return parts[0], parts[1] == "optional", nil
|
|
|
|
default:
|
|
|
|
return "", false, errors.Errorf("Error parsing %s: too many parts", tag)
|
2021-07-16 18:28:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
// allFields returns all the fields for a struct, including the ones from embedded structs
|
|
|
|
func allFields(ifv reflect.Value) []reflect.StructField {
|
|
|
|
if ifv.Kind() == reflect.Ptr {
|
|
|
|
ifv = ifv.Elem()
|
|
|
|
}
|
|
|
|
if ifv.Kind() != reflect.Struct {
|
|
|
|
return nil
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
var fields []reflect.StructField
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
if !ifv.IsValid() {
|
|
|
|
return nil
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
t := ifv.Type()
|
|
|
|
|
|
|
|
for i := 0; i < ifv.NumField(); i++ {
|
|
|
|
v := ifv.Field(i)
|
|
|
|
|
|
|
|
if v.Kind() == reflect.Struct && t.Field(i).Anonymous {
|
|
|
|
fields = append(fields, allFields(v)...)
|
|
|
|
continue
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
2021-08-03 19:56:54 +00:00
|
|
|
fields = append(fields, ifv.Type().Field(i))
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
return fields
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
// makeDecoder creates a decoder for the type for the struct passed on. If the struct has at least 1 json tag
|
|
|
|
// it'll unmarshall the body. If the struct has a `url` tag with value list-options it'll gather fleet.ListOptions
|
|
|
|
// from the URL. And finally, any other `url` tag will be treated as an ID from the URL path pattern, and it'll
|
|
|
|
// be decoded and set accordingly.
|
|
|
|
// IDs are expected to be uint, and can be optional by setting the tag as follows: `url:"some-id,optional"`
|
|
|
|
// list-options are optional by default and it'll ignore the optional portion of the tag.
|
|
|
|
func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc {
|
|
|
|
t := reflect.TypeOf(iface)
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
panic(fmt.Sprintf("makeDecoder only understands structs, not %T", iface))
|
|
|
|
}
|
|
|
|
|
2021-08-03 13:33:27 +00:00
|
|
|
return func(ctx context.Context, r *http.Request) (interface{}, error) {
|
2021-08-03 19:56:54 +00:00
|
|
|
v := reflect.New(t)
|
|
|
|
nilBody := false
|
|
|
|
|
|
|
|
buf := bufio.NewReader(r.Body)
|
|
|
|
if _, err := buf.Peek(1); err == io.EOF {
|
|
|
|
nilBody = true
|
|
|
|
} else {
|
|
|
|
req := v.Interface()
|
|
|
|
if err := json.NewDecoder(buf).Decode(req); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
v = reflect.ValueOf(req)
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
for _, f := range allFields(v) {
|
|
|
|
field := v.Elem().FieldByName(f.Name)
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
urlTagValue, ok := f.Tag.Lookup("url")
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
optional := false
|
|
|
|
var err error
|
|
|
|
if ok {
|
|
|
|
urlTagValue, optional, err = parseTag(urlTagValue)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
if ok && urlTagValue == "list_options" {
|
|
|
|
opts, err := listOptionsFromRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
field.Set(reflect.ValueOf(opts))
|
|
|
|
continue
|
|
|
|
}
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
if ok {
|
|
|
|
id, err := idFromRequest(r, urlTagValue)
|
|
|
|
if err != nil && err == errBadRoute && !optional {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
field.SetUint(uint64(id))
|
|
|
|
continue
|
|
|
|
}
|
2021-08-03 13:33:27 +00:00
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
_, jsonExpected := f.Tag.Lookup("json")
|
|
|
|
if jsonExpected && nilBody {
|
|
|
|
return nil, errors.New("Expected JSON Body")
|
|
|
|
}
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:56:54 +00:00
|
|
|
return v.Interface(), nil
|
|
|
|
}
|
2021-08-03 13:33:27 +00:00
|
|
|
}
|
|
|
|
|
2021-07-16 18:28:13 +00:00
|
|
|
func makeAuthenticatedServiceEndpoint(svc fleet.Service, f handlerFunc) endpoint.Endpoint {
|
|
|
|
return authenticatedUser(svc, makeServiceEndpoint(svc, f))
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeServiceEndpoint(svc fleet.Service, f handlerFunc) endpoint.Endpoint {
|
|
|
|
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
|
|
return f(ctx, request, svc)
|
|
|
|
}
|
|
|
|
}
|