15877: Accept JSON-encoded param values in JSON request body.
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 26 Nov 2019 20:32:30 +0000 (15:32 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 26 Nov 2019 20:32:30 +0000 (15:32 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

lib/controller/router/request.go
lib/controller/router/request_test.go

index 4d18395b6a87b7cc7aa421f78e118c18551453ca..cc6379486893742ff1f9464fcc8c62b1b5f8add7 100644 (file)
@@ -16,6 +16,45 @@ import (
        "github.com/gorilla/mux"
 )
 
+func guessAndParse(k, v string) (interface{}, error) {
+       // All of these form values arrive as strings, so we need some
+       // type-guessing to accept non-string inputs:
+       //
+       // Values for parameters that take ints (limit=1) or bools
+       // (include_trash=1) are parsed accordingly.
+       //
+       // "null" and "" are nil.
+       //
+       // Values that look like JSON objects, arrays, or strings are
+       // parsed as JSON.
+       //
+       // The rest are left as strings.
+       switch {
+       case intParams[k]:
+               return strconv.ParseInt(v, 10, 64)
+       case boolParams[k]:
+               return stringToBool(v), nil
+       case v == "null" || v == "":
+               return nil, nil
+       case strings.HasPrefix(v, "["):
+               var j []interface{}
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       case strings.HasPrefix(v, "{"):
+               var j map[string]interface{}
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       case strings.HasPrefix(v, "\""):
+               var j string
+               err := json.Unmarshal([]byte(v), &j)
+               return j, err
+       default:
+               return v, nil
+       }
+       // TODO: Need to accept "?foo[]=bar&foo[]=baz" as
+       // foo=["bar","baz"]?
+}
+
 // Parse req as an Arvados V1 API request and return the request
 // parameters.
 //
@@ -33,56 +72,11 @@ func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[st
        // Content-Type is application/x-www-form-urlencoded -- the
        // request body.
        for k, values := range req.Form {
-               // All of these form values arrive as strings, so we
-               // need some type-guessing to accept non-string
-               // inputs:
-               //
-               // Values for parameters that take ints (limit=1) or
-               // bools (include_trash=1) are parsed accordingly.
-               //
-               // "null" and "" are nil.
-               //
-               // Values that look like JSON objects, arrays, or
-               // strings are parsed as JSON.
-               //
-               // The rest are left as strings.
                for _, v := range values {
-                       switch {
-                       case intParams[k]:
-                               params[k], err = strconv.ParseInt(v, 10, 64)
-                               if err != nil {
-                                       return nil, err
-                               }
-                       case boolParams[k]:
-                               params[k] = stringToBool(v)
-                       case v == "null" || v == "":
-                               params[k] = nil
-                       case strings.HasPrefix(v, "["):
-                               var j []interface{}
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       case strings.HasPrefix(v, "{"):
-                               var j map[string]interface{}
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       case strings.HasPrefix(v, "\""):
-                               var j string
-                               err := json.Unmarshal([]byte(v), &j)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               params[k] = j
-                       default:
-                               params[k] = v
+                       params[k], err = guessAndParse(k, v)
+                       if err != nil {
+                               return nil, err
                        }
-                       // TODO: Need to accept "?foo[]=bar&foo[]=baz"
-                       // as foo=["bar","baz"]?
                }
        }
 
@@ -98,7 +92,20 @@ func (rtr *router) loadRequestParams(req *http.Request, attrsKey string) (map[st
                        return nil, httpError(http.StatusBadRequest, err)
                }
                for k, v := range jsonParams {
-                       params[k] = v
+                       switch v := v.(type) {
+                       case string:
+                               // The Ruby "arv" cli tool sends a
+                               // JSON-encode params map with
+                               // JSON-encoded values.
+                               dec, err := guessAndParse(k, v)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               jsonParams[k] = dec
+                               params[k] = dec
+                       default:
+                               params[k] = v
+                       }
                }
                if attrsKey != "" && params[attrsKey] == nil {
                        // Copy top-level parameters from JSON request
index 89238f656345aa2d7ccf19b4889ead4955f17034..118415cb40a2211c702ecf5fbfe0b0256da542b4 100644 (file)
@@ -49,10 +49,26 @@ func (tr *testReq) Request() *http.Request {
        } else if tr.json {
                if tr.jsonAttrsTop {
                        for k, v := range tr.attrs {
-                               param[k] = v
+                               if tr.jsonStringParam {
+                                       j, err := json.Marshal(v)
+                                       if err != nil {
+                                               panic(err)
+                                       }
+                                       param[k] = string(j)
+                               } else {
+                                       param[k] = v
+                               }
                        }
                } else if tr.attrs != nil {
-                       param[tr.attrsKey] = tr.attrs
+                       if tr.jsonStringParam {
+                               j, err := json.Marshal(tr.attrs)
+                               if err != nil {
+                                       panic(err)
+                               }
+                               param[tr.attrsKey] = string(j)
+                       } else {
+                               param[tr.attrsKey] = tr.attrs
+                       }
                }
                tr.body = bytes.NewBuffer(nil)
                err := json.NewEncoder(tr.body).Encode(param)
@@ -118,6 +134,8 @@ func (s *RouterSuite) TestAttrsInBody(c *check.C) {
        for _, tr := range []testReq{
                {attrsKey: "model_name", json: true, attrs: attrs},
                {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true},
+               {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: true, jsonStringParam: true},
+               {attrsKey: "model_name", json: true, attrs: attrs, jsonAttrsTop: false, jsonStringParam: true},
        } {
                c.Logf("tr: %#v", tr)
                req := tr.Request()