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  }