diff --git a/db.go b/db.go index 36fb38a94..d278cb2ef 100644 --- a/db.go +++ b/db.go @@ -478,7 +478,7 @@ func MergeSchemas(a, b []*DBInfo) []*DBInfo { // DBOptions represents options to set when initializing a db. type DBOptions struct { - ColumnLabel string `json:"columnLabel,omitempty"` + ColumnLabel string `json:"columnLabel,omitempty" valid:"required,string"` } // hasTime returns true if a contains a non-nil time. diff --git a/handler.go b/handler.go index 400d9c48d..eba6c51af 100644 --- a/handler.go +++ b/handler.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "bytes" "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa/internal" "github.com/pilosa/pilosa/pql" @@ -321,6 +322,16 @@ type sliceMaxResponse struct { // handlePostDB handles POST /db request. func (h *Handler) handlePostDB(w http.ResponseWriter, r *http.Request) { + var err error + // Copy request body for validation + buf, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) + r.Body = rdr2 + // Decode request. var req postDBRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -328,8 +339,15 @@ func (h *Handler) handlePostDB(w http.ResponseWriter, r *http.Request) { return } + // Validate request + err = h.validateRequest(buf, r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Create database. - _, err := h.Index.CreateDB(req.DB, req.Options) + _, err = h.Index.CreateDB(req.DB, req.Options) if err == ErrDatabaseExists { http.Error(w, err.Error(), http.StatusConflict) return @@ -344,9 +362,57 @@ func (h *Handler) handlePostDB(w http.ResponseWriter, r *http.Request) { } } +// Validate request body for db/frame creation +func (h *Handler) validateRequest(r []byte, path string) error { + var data map[string]interface{} + + err := json.Unmarshal(r, &data) + if err != nil { + return err + } + + option, ok := data["options"] + if len(data) >= 2 && !ok { + return errors.New("options needs to be provided") + } else if ok { + err = h.validateOptions(path, option.(map[string]interface{})) + if err != nil { + return fmt.Errorf("invalid options: %s", option.(map[string]interface{})) + } + } + + return nil + +} + +// Validate options in request body for db/frame creation, make sure key and value for columnLabel/rowLable is correct +func (h Handler) validateOptions(path string, options map[string]interface{}) error { + switch path { + case "/db": + if _, ok := options["columnLabel"]; !ok && len(options) > 0 { + return errors.New("columnLabel is not provided") + } else if ok { + err := ValidateName(options["columnLabel"].(string)) + if err != nil { + return err + } + } + case "/frame": + if _, ok := options["rowLabel"]; !ok && len(options) > 0 { + return errors.New("rowLabel is not provided") + } else if ok { + err := ValidateName(options["rowLabel"].(string)) + if err != nil { + return err + } + } + } + return nil +} + type postDBRequest struct { DB string `json:"db"` - Options DBOptions `json:"options"` + Options DBOptions `json:"options" valid:"json"` } type postDBResponse struct{} @@ -478,6 +544,23 @@ type postDBAttrDiffResponse struct { // handlePostFrame handles POST /frame request. func (h *Handler) handlePostFrame(w http.ResponseWriter, r *http.Request) { + var err error + // Copy request body for validation + buf, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) + r.Body = rdr2 + + // Validate request + err = h.validateRequest(buf, r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Decode request. var req postFrameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -493,7 +576,7 @@ func (h *Handler) handlePostFrame(w http.ResponseWriter, r *http.Request) { } // Create frame. - _, err := db.CreateFrame(req.Frame, req.Options) + _, err = db.CreateFrame(req.Frame, req.Options) if err == ErrFrameExists { http.Error(w, err.Error(), http.StatusConflict) return diff --git a/handler_test.go b/handler_test.go index 20f76de05..c6d181f44 100644 --- a/handler_test.go +++ b/handler_test.go @@ -14,6 +14,7 @@ import ( "strings" "testing" + "fmt" "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/internal" @@ -868,3 +869,88 @@ func MustReadAll(r io.Reader) []byte { } return buf } + +// Ensure that options needs to be provided to set columnLabel when create DB +func TestHandler_DB_Options(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + + s := NewServer() + s.Handler.Index = idx.Index + defer s.Close() + + resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", s.URL+"/db", strings.NewReader(`{"db": "sample-db", "columnLabel": "location"}`))) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + // Verify body response. + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } else if buf, err := ioutil.ReadAll(resp.Body); err != nil { + t.Fatal(err) + } else if string(buf) != "options needs to be provided"+"\n" { + fmt.Println(string(buf) == "options needs to be provided") + t.Fatalf("unexpected response body: %s", buf) + } + +} + +// Ensure that rowLabel is provided as an options when create frame +func TestHandler_Frame_Options(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + + s := NewServer() + s.Handler.Index = idx.Index + defer s.Close() + + // Create database. + if _, err := idx.CreateDBIfNotExists("sample-db", pilosa.DBOptions{}); err != nil { + t.Fatal(err) + } + resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", s.URL+"/frame", strings.NewReader(`{"db": "sample-db", "frame": "test", "options": {"columnLabel": "location"}}`))) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + // Verify body response. + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } else if buf, err := ioutil.ReadAll(resp.Body); err != nil { + t.Fatal(err) + } else if string(buf) != "invalid options: map[columnLabel:location]"+"\n" { + t.Fatalf("unexpected response body: %s", buf) + } +} + +// Ensure that rowLabel is provided as an options when create frame +func TestHandler_OptionsValue(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + + s := NewServer() + s.Handler.Index = idx.Index + defer s.Close() + + // Create database. + if _, err := idx.CreateDBIfNotExists("sample-db", pilosa.DBOptions{}); err != nil { + t.Fatal(err) + } + resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", s.URL+"/frame", strings.NewReader(`{"db": "sample-db", "options": {"rowLabel": "///"}}`))) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + // Verify body response. + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } else if buf, err := ioutil.ReadAll(resp.Body); err != nil { + t.Fatal(err) + } else if string(buf) != "invalid options: map[rowLabel:///]"+"\n" { + t.Fatalf("unexpected response body: %s", buf) + } +}