github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/data/accounts.go (about) 1 package data 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 "strings" 8 9 "github.com/cozy/cozy-stack/model/account" 10 "github.com/cozy/cozy-stack/model/oauth" 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/jsonapi" 15 "github.com/cozy/cozy-stack/pkg/metadata" 16 "github.com/cozy/cozy-stack/web/middlewares" 17 "github.com/labstack/echo/v4" 18 ) 19 20 // XXX: it would be better to have specific routes for managing accounts. The 21 // overriding of the /data/io.cozy.accounts/* routes is here mainly for 22 // retro-compatible reasons, but specific routes would improve the API. 23 24 func getAccount(c echo.Context) error { 25 instance := middlewares.GetInstance(c) 26 doctype := consts.Accounts 27 docid := c.Get("docid").(string) 28 if docid == "" { 29 return dbStatus(c) 30 } 31 32 var out couchdb.JSONDoc 33 var err error 34 rev := c.QueryParam("rev") 35 if rev != "" { 36 err = couchdb.GetDoc(instance, consts.SoftDeletedAccounts, docid, &out) 37 if err == nil && out.M["soft_deleted_rev"] != rev { 38 err = errors.New("invalid rev") 39 } 40 if err != nil { 41 err = couchdb.GetDocRev(instance, doctype, docid, rev, &out) 42 } 43 } else { 44 err = couchdb.GetDoc(instance, doctype, docid, &out) 45 } 46 if err != nil { 47 return fixErrorNoDatabaseIsWrongDoctype(err) 48 } 49 out.Type = doctype 50 51 if err = middlewares.Allow(c, permission.GET, &out); err != nil { 52 return err 53 } 54 55 if account.Encrypt(out) { 56 if err = couchdb.UpdateDoc(instance, &out); err != nil { 57 return err 58 } 59 } 60 61 perm, err := middlewares.GetPermission(c) 62 if err != nil { 63 return err 64 } 65 if perm.Type == permission.TypeKonnector || 66 (c.QueryParam("include") == "credentials" && perm.Type == permission.TypeWebapp) { 67 // The account decryption is allowed for konnectors or for apps services 68 account.Decrypt(out) 69 } 70 71 return c.JSON(http.StatusOK, out.ToMapWithType()) 72 } 73 74 func updateAccount(c echo.Context) error { 75 instance := middlewares.GetInstance(c) 76 docid := c.Get("docid").(string) 77 78 var doc couchdb.JSONDoc 79 if err := json.NewDecoder(c.Request().Body).Decode(&doc); err != nil { 80 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 81 } 82 83 doc.Type = consts.Accounts 84 85 if (doc.ID() == "") != (doc.Rev() == "") { 86 return jsonapi.NewError(http.StatusBadRequest, 87 "You must either provide an _id and _rev in document (update) or neither (create with fixed id).") 88 } 89 90 if doc.ID() != "" && doc.ID() != docid { 91 return jsonapi.NewError(http.StatusBadRequest, "document _id doesnt match url") 92 } 93 94 if doc.ID() == "" { 95 doc.SetID(docid) 96 return createNamedDoc(c, doc) 97 } 98 99 errWhole := middlewares.AllowWholeType(c, permission.PUT, doc.DocType()) 100 if errWhole != nil { 101 // we cant apply to whole type, let's fetch old doc and see if it applies there 102 var old couchdb.JSONDoc 103 errFetch := couchdb.GetDoc(instance, doc.DocType(), doc.ID(), &old) 104 if errFetch != nil { 105 return errFetch 106 } 107 old.Type = doc.DocType() 108 // check if permissions set allows manipulating old doc 109 errOld := middlewares.Allow(c, permission.PUT, &old) 110 if errOld != nil { 111 return errOld 112 } 113 114 // also check if permissions set allows manipulating new doc 115 errNew := middlewares.Allow(c, permission.PUT, &doc) 116 if errNew != nil { 117 return errNew 118 } 119 } 120 121 account.Encrypt(doc) 122 123 if doc.M["cozyMetadata"] == nil { 124 // This is not the expected type for a JSON doc but it should work since it 125 // will be marshalled when saved. 126 doc.M["cozyMetadata"] = CozyMetadataFromClaims(c) 127 } 128 129 errUpdate := couchdb.UpdateDoc(instance, &doc) 130 if errUpdate != nil { 131 return fixErrorNoDatabaseIsWrongDoctype(errUpdate) 132 } 133 134 perm, err := middlewares.GetPermission(c) 135 if err != nil { 136 return err 137 } 138 if perm.Type == permission.TypeKonnector { 139 account.Decrypt(doc) 140 } 141 142 return c.JSON(http.StatusOK, echo.Map{ 143 "ok": true, 144 "id": doc.ID(), 145 "rev": doc.Rev(), 146 "type": doc.DocType(), 147 "data": doc.ToMapWithType(), 148 }) 149 } 150 151 func createAccount(c echo.Context) error { 152 doctype := consts.Accounts 153 instance := middlewares.GetInstance(c) 154 155 doc := couchdb.JSONDoc{Type: doctype} 156 if err := json.NewDecoder(c.Request().Body).Decode(&doc.M); err != nil { 157 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 158 } 159 160 if err := middlewares.Allow(c, permission.POST, &doc); err != nil { 161 return err 162 } 163 164 account.Encrypt(doc) 165 account.ComputeName(doc) 166 167 // This is not the expected type for a JSON doc but it should work since it 168 // will be marshalled when saved. 169 doc.M["cozyMetadata"] = CozyMetadataFromClaims(c) 170 171 if err := couchdb.CreateDoc(instance, &doc); err != nil { 172 return err 173 } 174 175 return c.JSON(http.StatusCreated, echo.Map{ 176 "ok": true, 177 "id": doc.ID(), 178 "rev": doc.Rev(), 179 "type": doc.DocType(), 180 "data": doc.ToMapWithType(), 181 }) 182 } 183 184 // CozyMetadataFromClaims returns a CozyMetadata struct, with the app fields 185 // filled with information from the permission claims. 186 func CozyMetadataFromClaims(c echo.Context) *metadata.CozyMetadata { 187 cm := metadata.New() 188 189 var slug, version string 190 if claims := c.Get("claims"); claims != nil { 191 cl := claims.(permission.Claims) 192 switch cl.AudienceString() { 193 case consts.AppAudience, consts.KonnectorAudience: 194 slug = cl.Subject 195 case consts.AccessTokenAudience: 196 if perms, err := middlewares.GetPermission(c); err == nil { 197 if cli, ok := perms.Client.(*oauth.Client); ok { 198 slug = oauth.GetLinkedAppSlug(cli.SoftwareID) 199 // Special case for cozy-desktop: it is an OAuth app not linked 200 // to a web app, so it has no slug, but we still want to keep 201 // in cozyMetadata its changes, so we use a fake slug. 202 if slug == "" && strings.Contains(cli.SoftwareID, "cozy-desktop") { 203 slug = "cozy-desktop" 204 } 205 version = cli.SoftwareVersion 206 } 207 } 208 } 209 } 210 211 if slug != "" { 212 cm.CreatedByApp = slug 213 cm.CreatedByAppVersion = version 214 cm.UpdatedByApps = []*metadata.UpdatedByAppEntry{ 215 { 216 Slug: slug, 217 Version: version, 218 Date: cm.UpdatedAt, 219 }, 220 } 221 } 222 223 return cm 224 }