github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/data/data.go (about) 1 // Package data provide simple CRUD operation on couchdb doc 2 package data 3 4 import ( 5 "encoding/json" 6 "net/http" 7 "net/url" 8 "strconv" 9 "strings" 10 11 "github.com/cozy/cozy-stack/model/permission" 12 "github.com/cozy/cozy-stack/pkg/consts" 13 "github.com/cozy/cozy-stack/pkg/couchdb" 14 "github.com/cozy/cozy-stack/pkg/couchdb/stream" 15 "github.com/cozy/cozy-stack/pkg/jsonapi" 16 "github.com/cozy/cozy-stack/web/files" 17 "github.com/cozy/cozy-stack/web/middlewares" 18 "github.com/labstack/echo/v4" 19 ) 20 21 func paramIsTrue(c echo.Context, param string) bool { 22 return c.QueryParam(param) == "true" 23 } 24 25 // ValidDoctype validates the doctype and sets it in the context of the request. 26 func ValidDoctype(next echo.HandlerFunc) echo.HandlerFunc { 27 return func(c echo.Context) error { 28 doctype := c.Param("doctype") 29 if doctype == "" { 30 return jsonapi.Errorf(http.StatusBadRequest, "Invalid doctype '%s'", doctype) 31 } 32 33 docidraw := c.Param("docid") 34 docid, err := url.QueryUnescape(docidraw) 35 if err != nil { 36 return jsonapi.Errorf(http.StatusBadRequest, "Invalid docid '%s'", docid) 37 } 38 c.Set("docid", docid) 39 40 return next(c) 41 } 42 } 43 44 func fixErrorNoDatabaseIsWrongDoctype(err error) error { 45 if couchdb.IsNoDatabaseError(err) { 46 err.(*couchdb.Error).Reason = "wrong_doctype" 47 } 48 return err 49 } 50 51 func allDoctypes(c echo.Context) error { 52 instance := middlewares.GetInstance(c) 53 54 if err := middlewares.AllowWholeType(c, permission.GET, consts.Doctypes); err != nil { 55 return err 56 } 57 58 types, err := couchdb.AllDoctypes(instance) 59 if err != nil { 60 return err 61 } 62 var doctypes []string 63 for _, typ := range types { 64 if permission.CheckReadable(typ) == nil { 65 doctypes = append(doctypes, typ) 66 } 67 } 68 return c.JSON(http.StatusOK, doctypes) 69 } 70 71 // GetDoc get a doc by its type and id 72 func getDoc(c echo.Context) error { 73 instance := middlewares.GetInstance(c) 74 doctype := c.Param("doctype") 75 docid := c.Get("docid").(string) 76 77 // Accounts are handled specifically to remove the auth fields 78 if doctype == consts.Accounts { 79 return getAccount(c) 80 } 81 82 if err := permission.CheckReadable(doctype); err != nil { 83 return err 84 } 85 86 if docid == "" { 87 return dbStatus(c) 88 } 89 90 if paramIsTrue(c, "revs") { 91 return proxy(c, docid) 92 } 93 94 var out couchdb.JSONDoc 95 err := couchdb.GetDoc(instance, doctype, docid, &out) 96 out.Type = doctype 97 if err != nil { 98 if couchdb.IsNotFoundError(err) { 99 if err := middlewares.Allow(c, permission.GET, &out); err != nil { 100 return err 101 } 102 } 103 return fixErrorNoDatabaseIsWrongDoctype(err) 104 } 105 106 if err := middlewares.Allow(c, permission.GET, &out); err != nil { 107 // Allow to read the bitwarden settings document with only a permission 108 // bitwarden organizations doctype 109 if doctype == consts.Settings && docid == consts.BitwardenSettingsID { 110 err = middlewares.AllowWholeType(c, permission.GET, consts.BitwardenOrganizations) 111 } 112 if err != nil { 113 return err 114 } 115 } 116 117 return c.JSON(http.StatusOK, out.ToMapWithType()) 118 } 119 120 // CreateDoc create doc from the json passed as body 121 func createDoc(c echo.Context) error { 122 doctype := c.Param("doctype") 123 instance := middlewares.GetInstance(c) 124 125 // Accounts are handled specifically to remove the auth fields 126 if doctype == consts.Accounts { 127 return createAccount(c) 128 } 129 130 doc := couchdb.JSONDoc{Type: doctype} 131 if err := json.NewDecoder(c.Request().Body).Decode(&doc.M); err != nil { 132 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 133 } 134 135 if err := permission.CheckWritable(doctype); err != nil { 136 return err 137 } 138 139 if err := middlewares.Allow(c, permission.POST, &doc); err != nil { 140 return err 141 } 142 143 if err := couchdb.CreateDoc(instance, &doc); err != nil { 144 return err 145 } 146 147 return c.JSON(http.StatusCreated, echo.Map{ 148 "ok": true, 149 "id": doc.ID(), 150 "rev": doc.Rev(), 151 "type": doc.DocType(), 152 "data": doc.ToMapWithType(), 153 }) 154 } 155 156 func createNamedDoc(c echo.Context, doc couchdb.JSONDoc) error { 157 instance := middlewares.GetInstance(c) 158 159 err := middlewares.Allow(c, permission.POST, &doc) 160 if err != nil { 161 return err 162 } 163 164 err = couchdb.CreateNamedDocWithDB(instance, &doc) 165 if err != nil { 166 return fixErrorNoDatabaseIsWrongDoctype(err) 167 } 168 169 return c.JSON(http.StatusOK, echo.Map{ 170 "ok": true, 171 "id": doc.ID(), 172 "rev": doc.Rev(), 173 "type": doc.DocType(), 174 "data": doc.ToMapWithType(), 175 }) 176 } 177 178 // UpdateDoc updates the document given in the request or creates a new one with 179 // the given id. 180 func UpdateDoc(c echo.Context) error { 181 instance := middlewares.GetInstance(c) 182 doctype := c.Param("doctype") 183 docid := c.Get("docid").(string) 184 185 // Accounts are handled specifically to remove the auth fields 186 if doctype == consts.Accounts { 187 return updateAccount(c) 188 } 189 190 var doc couchdb.JSONDoc 191 if err := json.NewDecoder(c.Request().Body).Decode(&doc); err != nil { 192 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 193 } 194 195 doc.Type = doctype 196 197 if err := permission.CheckWritable(doc.Type); err != nil { 198 return err 199 } 200 201 if (doc.ID() == "") != (doc.Rev() == "") { 202 return jsonapi.Errorf(http.StatusBadRequest, 203 "You must either provide an _id and _rev in document (update) or neither (create with fixed id).") 204 } 205 206 if doc.ID() != "" && doc.ID() != docid { 207 return jsonapi.Errorf(http.StatusBadRequest, "document _id doesnt match url") 208 } 209 210 if doc.ID() == "" { 211 doc.SetID(docid) 212 return createNamedDoc(c, doc) 213 } 214 215 errWhole := middlewares.AllowWholeType(c, permission.PUT, doc.DocType()) 216 if errWhole != nil { 217 // we cant apply to whole type, let's fetch old doc and see if it applies there 218 var old couchdb.JSONDoc 219 errFetch := couchdb.GetDoc(instance, doc.DocType(), doc.ID(), &old) 220 if errFetch != nil { 221 return errFetch 222 } 223 old.Type = doc.DocType() 224 // check if permissions set allows manipulating old doc 225 errOld := middlewares.Allow(c, permission.PUT, &old) 226 if errOld != nil { 227 return errOld 228 } 229 230 // also check if permissions set allows manipulating new doc 231 errNew := middlewares.Allow(c, permission.PUT, &doc) 232 if errNew != nil { 233 return errNew 234 } 235 } 236 237 errUpdate := couchdb.UpdateDoc(instance, &doc) 238 if errUpdate != nil { 239 return fixErrorNoDatabaseIsWrongDoctype(errUpdate) 240 } 241 242 return c.JSON(http.StatusOK, echo.Map{ 243 "ok": true, 244 "id": doc.ID(), 245 "rev": doc.Rev(), 246 "type": doc.DocType(), 247 "data": doc.ToMapWithType(), 248 }) 249 } 250 251 // DeleteDoc deletes the provided document from its database. 252 func DeleteDoc(c echo.Context) error { 253 instance := middlewares.GetInstance(c) 254 doctype := c.Param("doctype") 255 docid := c.Get("docid").(string) 256 revHeader := c.Request().Header.Get("If-Match") 257 revQuery := c.QueryParam("rev") 258 rev := "" 259 260 if revHeader != "" && revQuery != "" && revQuery != revHeader { 261 return jsonapi.Errorf(http.StatusBadRequest, 262 "If-Match Header and rev query parameters mismatch") 263 } else if revHeader != "" { 264 rev = revHeader 265 } else if revQuery != "" { 266 rev = revQuery 267 } else { 268 return jsonapi.Errorf(http.StatusBadRequest, "delete without revision") 269 } 270 271 if err := permission.CheckWritable(doctype); err != nil { 272 return err 273 } 274 275 var doc couchdb.JSONDoc 276 err := couchdb.GetDoc(instance, doctype, docid, &doc) 277 if err != nil { 278 return err 279 } 280 doc.Type = doctype 281 doc.SetRev(rev) 282 283 err = middlewares.Allow(c, permission.DELETE, &doc) 284 if err != nil { 285 return err 286 } 287 288 err = couchdb.DeleteDoc(instance, &doc) 289 if err != nil { 290 return fixErrorNoDatabaseIsWrongDoctype(err) 291 } 292 293 return c.JSON(http.StatusOK, echo.Map{ 294 "ok": true, 295 "id": doc.ID(), 296 "rev": doc.Rev(), 297 "type": doc.DocType(), 298 "deleted": true, 299 }) 300 } 301 302 // DeleteDatabase deletes the doctype's database. 303 func DeleteDatabase(c echo.Context) error { 304 instance := middlewares.GetInstance(c) 305 doctype := c.Param("doctype") 306 307 if err := permission.CheckWritable(doctype); err != nil { 308 return err 309 } 310 if err := middlewares.AllowWholeType(c, permission.DELETE, doctype); err != nil { 311 return err 312 } 313 if err := couchdb.DeleteDB(instance, doctype); err != nil { 314 return err 315 } 316 return c.JSON(http.StatusOK, echo.Map{ 317 "ok": true, 318 "deleted": true, 319 }) 320 } 321 322 func defineIndex(c echo.Context) error { 323 instance := middlewares.GetInstance(c) 324 doctype := c.Param("doctype") 325 326 var definitionRequest map[string]interface{} 327 if err := json.NewDecoder(c.Request().Body).Decode(&definitionRequest); err != nil { 328 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 329 } 330 331 if err := permission.CheckReadable(doctype); err != nil { 332 return err 333 } 334 335 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 336 return err 337 } 338 339 result, err := couchdb.DefineIndexRaw(instance, doctype, &definitionRequest) 340 if err != nil { 341 return err 342 } 343 344 return c.JSON(http.StatusOK, result) 345 } 346 347 func findDocuments(c echo.Context) error { 348 instance := middlewares.GetInstance(c) 349 doctype := c.Param("doctype") 350 351 var findRequest map[string]interface{} 352 if err := json.NewDecoder(c.Request().Body).Decode(&findRequest); err != nil { 353 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 354 } 355 356 if err := permission.CheckReadable(doctype); err != nil { 357 return err 358 } 359 360 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 361 return err 362 } 363 364 limit, hasLimit := findRequest["limit"].(float64) 365 if !hasLimit || limit > consts.MaxItemsPerPageForMango { 366 limit = 100 367 } 368 findRequest["limit"] = limit 369 370 var results []couchdb.JSONDoc 371 resp, err := couchdb.FindDocsRaw(instance, doctype, &findRequest, &results) 372 if err != nil { 373 return err 374 } 375 // There might be more docs next when the returned docs reached the limit 376 next := len(results) >= int(limit) 377 out := echo.Map{ 378 "docs": results, 379 "limit": limit, 380 "next": next, 381 "bookmark": resp.Bookmark, 382 } 383 if resp.ExecutionStats != nil { 384 out["execution_stats"] = resp.ExecutionStats 385 } 386 if resp.Warning != "" { 387 out["warning"] = resp.Warning 388 } 389 return c.JSON(http.StatusOK, out) 390 } 391 392 func allDocs(c echo.Context) error { 393 doctype := c.Param("doctype") 394 if err := permission.CheckReadable(doctype); err != nil { 395 return err 396 } 397 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 398 return err 399 } 400 401 if c.QueryParam("Fields") == "" && c.QueryParam("DesignDocs") == "" { 402 // Fast path, just proxy the request/response 403 return proxy(c, "_all_docs") 404 } 405 406 inst := middlewares.GetInstance(c) 407 limit, _ := strconv.Atoi(c.QueryParam("limit")) 408 skip, _ := strconv.Atoi(c.QueryParam("skip")) 409 req := &couchdb.AllDocsRequest{ 410 Descending: c.QueryParam("descending") == "true", 411 Limit: limit, 412 Skip: skip, 413 StartKey: c.QueryParam("startkey"), 414 EndKey: c.QueryParam("endkey"), 415 } 416 body, err := couchdb.MakeAllDocsRequest(inst, doctype, req) 417 if err != nil { 418 return c.JSON(http.StatusInternalServerError, echo.Map{"error": err}) 419 } 420 defer body.Close() 421 422 fields := strings.Split(c.QueryParam("Fields"), ",") 423 filter := stream.NewAllDocsFilter(fields) 424 if c.QueryParam("DesignDocs") == "false" { 425 filter.SkipDesignDocs() 426 } 427 c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 428 c.Response().WriteHeader(http.StatusOK) 429 if err := filter.Stream(body, c.Response()); err != nil { 430 inst.Logger().WithNamespace("couchdb").Warnf("error on all_docs: %s", err) 431 return err 432 } 433 return nil 434 } 435 436 func normalDocs(c echo.Context) error { 437 instance := middlewares.GetInstance(c) 438 doctype := c.Param("doctype") 439 if err := permission.CheckReadable(doctype); err != nil { 440 return err 441 } 442 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 443 return err 444 } 445 skip, err := strconv.ParseInt(c.QueryParam("skip"), 10, 64) 446 if err != nil || skip < 0 { 447 skip = 0 448 } 449 bookmark := c.QueryParam("bookmark") 450 limit, err := strconv.ParseInt(c.QueryParam("limit"), 10, 64) 451 if err != nil || limit < 0 || limit > consts.MaxItemsPerPageForMango { 452 limit = 100 453 } 454 executionStats, err := strconv.ParseBool(c.QueryParam("execution_stats")) 455 if err != nil { 456 executionStats = false 457 } 458 res, err := couchdb.NormalDocs(instance, doctype, int(skip), int(limit), bookmark, executionStats) 459 if err != nil { 460 return err 461 } 462 return c.JSON(http.StatusOK, res) 463 } 464 465 func getDesignDoc(c echo.Context) error { 466 doctype := c.Param("doctype") 467 ddoc := c.Param("designdocid") 468 469 if err := permission.CheckReadable(doctype); err != nil { 470 return err 471 } 472 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 473 return err 474 } 475 return proxy(c, "_design/"+ddoc) 476 } 477 478 func getDesignDocs(c echo.Context) error { 479 doctype := c.Param("doctype") 480 if err := permission.CheckReadable(doctype); err != nil { 481 return err 482 } 483 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 484 return err 485 } 486 return proxy(c, "_design_docs") 487 } 488 489 func copyDesignDoc(c echo.Context) error { 490 instance := middlewares.GetInstance(c) 491 doctype := c.Param("doctype") 492 ddoc := c.Param("designdocid") 493 494 header := c.Request().Header 495 destination := header.Get("Destination") 496 if destination == "" { 497 return c.JSON(http.StatusBadRequest, "You must set a Destination header") 498 } 499 if err := permission.CheckReadable(doctype); err != nil { 500 return err 501 } 502 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 503 return err 504 } 505 path := "_design/" + ddoc 506 res, err := couchdb.Copy(instance, doctype, path, destination) 507 if err != nil { 508 return err 509 } 510 return c.JSON(http.StatusCreated, res) 511 } 512 513 func deleteDesignDoc(c echo.Context) error { 514 doctype := c.Param("doctype") 515 ddoc := c.Param("designdocid") 516 517 if err := permission.CheckReadable(doctype); err != nil { 518 return err 519 } 520 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 521 return err 522 } 523 if c.QueryParam("rev") == "" { 524 return c.JSON(http.StatusBadRequest, echo.Map{ 525 "error": "You must pass a rev param", 526 }) 527 } 528 if !couchdb.CheckDesignDocCanBeDeleted(doctype, ddoc) { 529 return c.JSON(http.StatusForbidden, echo.Map{ 530 "error": "This design doc cannot be deleted", 531 }) 532 } 533 return proxy(c, "_design/"+ddoc) 534 } 535 536 // mostly just to prevent couchdb crash on replications 537 func dataAPIWelcome(c echo.Context) error { 538 return c.JSON(http.StatusOK, echo.Map{ 539 "message": "welcome to a cozy API", 540 }) 541 } 542 543 func couchdbStyleErrorHandler(next echo.HandlerFunc) echo.HandlerFunc { 544 return func(c echo.Context) error { 545 err := next(c) 546 if err == nil { 547 return nil 548 } 549 550 if ce, ok := err.(*couchdb.Error); ok { 551 return c.JSON(ce.StatusCode, ce.JSON()) 552 } 553 554 if he, ok := err.(*echo.HTTPError); ok { 555 return c.JSON(he.Code, echo.Map{"error": he.Error()}) 556 } 557 558 if je, ok := err.(*jsonapi.Error); ok { 559 return c.JSON(je.Status, echo.Map{"error": je.Error()}) 560 } 561 562 return c.JSON(http.StatusInternalServerError, echo.Map{ 563 "error": err.Error(), 564 }) 565 } 566 } 567 568 // Routes sets the routing for the data service 569 func Routes(router *echo.Group) { 570 router.Use(couchdbStyleErrorHandler) 571 572 // API Routes that don't depend on a doctype 573 router.GET("/", dataAPIWelcome) 574 router.GET("/_all_doctypes", allDoctypes) 575 576 // API Routes under /:doctype 577 group := router.Group("/:doctype", ValidDoctype) 578 579 replicationRoutes(group) 580 files.ReferencesRoutes(group) 581 files.NotSynchronizedOnRoutes(group) 582 583 group.GET("/:docid", getDoc) 584 group.PUT("/:docid", UpdateDoc) 585 group.DELETE("/:docid", DeleteDoc) 586 group.POST("/", createDoc) 587 group.GET("/_all_docs", allDocs) 588 group.POST("/_all_docs", allDocs) 589 group.GET("/_normal_docs", normalDocs) 590 group.POST("/_index", defineIndex) 591 group.POST("/_find", findDocuments) 592 593 group.GET("/_design/:designdocid", getDesignDoc) 594 group.GET("/_design_docs", getDesignDocs) 595 group.POST("/_design/:designdocid/copy", copyDesignDoc) 596 group.DELETE("/_design/:designdocid", deleteDesignDoc) 597 598 group.DELETE("/", DeleteDatabase) 599 }