github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/permissions/permissions.go (about) 1 // Package permissions is the HTTP handlers for managing the permissions on a 2 // Cozy (creating a share by link for example). 3 package permissions 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/cozy/cozy-stack/model/oauth" 14 "github.com/cozy/cozy-stack/model/permission" 15 "github.com/cozy/cozy-stack/model/sharing" 16 "github.com/cozy/cozy-stack/pkg/consts" 17 "github.com/cozy/cozy-stack/pkg/couchdb" 18 "github.com/cozy/cozy-stack/pkg/crypto" 19 "github.com/cozy/cozy-stack/pkg/jsonapi" 20 "github.com/cozy/cozy-stack/pkg/metadata" 21 "github.com/cozy/cozy-stack/pkg/prefixer" 22 "github.com/cozy/cozy-stack/web/middlewares" 23 "github.com/justincampbell/bigduration" 24 "github.com/labstack/echo/v4" 25 ) 26 27 // ErrPatchCodeOrSet is returned when an attempt is made to patch both 28 // code & set in one request 29 var ErrPatchCodeOrSet = echo.NewHTTPError(http.StatusBadRequest, 30 "The patch doc should have property 'codes' or 'permissions', not both") 31 32 // ContextPermissionSet is the key used in echo context to store permissions set 33 const ContextPermissionSet = "permissions_set" 34 35 // ContextClaims is the key used in echo context to store claims 36 const ContextClaims = "token_claims" 37 38 // APIPermission is the struct that will be used to serialized a permission to 39 // JSON-API 40 type APIPermission struct { 41 *permission.Permission 42 included []jsonapi.Object 43 } 44 45 // MarshalJSON implements jsonapi.Doc 46 func (p *APIPermission) MarshalJSON() ([]byte, error) { 47 return json.Marshal(p.Permission) 48 } 49 50 // Relationships implements jsonapi.Doc 51 func (p *APIPermission) Relationships() jsonapi.RelationshipMap { return nil } 52 53 // Included implements jsonapi.Doc 54 func (p *APIPermission) Included() []jsonapi.Object { return p.included } 55 56 // Links implements jsonapi.Doc 57 func (p *APIPermission) Links() *jsonapi.LinksList { 58 links := &jsonapi.LinksList{Self: "/permissions/" + p.PID} 59 parts := strings.SplitN(p.SourceID, "/", 2) 60 if parts[0] == consts.Sharings { 61 links.Related = "/sharings/" + parts[1] 62 } 63 return links 64 } 65 66 type apiMember struct { 67 *sharing.Member 68 } 69 70 func (m *apiMember) ID() string { return "" } 71 func (m *apiMember) Rev() string { return "" } 72 func (m *apiMember) SetID(id string) {} 73 func (m *apiMember) SetRev(rev string) {} 74 func (m *apiMember) DocType() string { return consts.SharingsMembers } 75 func (m *apiMember) Clone() couchdb.Doc { cloned := *m; return &cloned } 76 func (m *apiMember) Relationships() jsonapi.RelationshipMap { return nil } 77 func (m *apiMember) Included() []jsonapi.Object { return nil } 78 func (m *apiMember) Links() *jsonapi.LinksList { return nil } 79 80 type getPermsFunc func(db prefixer.Prefixer, id string) (*permission.Permission, error) 81 82 func displayPermissions(c echo.Context) error { 83 doc, err := middlewares.GetPermission(c) 84 if err != nil { 85 return err 86 } 87 88 // Include the sharing member (when relevant) 89 var included []jsonapi.Object 90 if doc.Type == permission.TypeSharePreview || doc.Type == permission.TypeShareInteract { 91 inst := middlewares.GetInstance(c) 92 sharingID := strings.TrimPrefix(doc.SourceID, consts.Sharings+"/") 93 if s, err := sharing.FindSharing(inst, sharingID); err == nil { 94 sharecode := middlewares.GetRequestToken(c) 95 if member, err := s.FindMemberByCode(doc, sharecode); err == nil { 96 included = []jsonapi.Object{&apiMember{member}} 97 } 98 } 99 } 100 101 // XXX hides the codes and password hash in the response 102 doc.Codes = nil 103 doc.ShortCodes = nil 104 if doc.Password != nil { 105 doc.Password = true 106 } 107 return jsonapi.Data(c, http.StatusOK, &APIPermission{doc, included}, nil) 108 } 109 110 func createPermission(c echo.Context) error { 111 instance := middlewares.GetInstance(c) 112 names := strings.Split(c.QueryParam("codes"), ",") 113 ttl := c.QueryParam("ttl") 114 tiny, _ := strconv.ParseBool(c.QueryParam("tiny")) 115 116 parent, err := middlewares.GetPermission(c) 117 if err != nil { 118 return err 119 } 120 121 var slug string 122 sourceID := parent.SourceID 123 // Check if the permission is linked to an OAuth Client 124 if parent.Client != nil { 125 oauthClient := parent.Client.(*oauth.Client) 126 if slug = oauth.GetLinkedAppSlug(oauthClient.SoftwareID); slug != "" { 127 // Changing the sourceID from the OAuth clientID to the classic 128 // io.cozy.apps/slug one 129 sourceID = consts.Apps + "/" + slug 130 } 131 } 132 133 var subdoc permission.Permission 134 if _, err = jsonapi.Bind(c.Request().Body, &subdoc); err != nil { 135 return err 136 } 137 138 var expiresAt *time.Time 139 if ttl != "" { 140 if d, errd := bigduration.ParseDuration(ttl); errd == nil { 141 ex := time.Now().Add(d) 142 expiresAt = &ex 143 if d.Hours() > 1.0 && tiny { 144 instance.Logger().Info("Tiny can not be set to true since duration > 1h") 145 tiny = false 146 } 147 } 148 } 149 150 var codes map[string]string 151 var shortcodes map[string]string 152 153 if names != nil { 154 codes = make(map[string]string, len(names)) 155 shortcodes = make(map[string]string, len(names)) 156 for _, name := range names { 157 longcode, err := instance.CreateShareCode(name) 158 shortcode := createShortCode(tiny) 159 160 codes[name] = longcode 161 shortcodes[name] = shortcode 162 if err != nil { 163 return err 164 } 165 } 166 } 167 168 if parent == nil { 169 return echo.NewHTTPError(http.StatusUnauthorized, "no parent") 170 } 171 172 // Getting the slug from the token if it has not been retrieved before 173 // with the linkedapp 174 if slug == "" { 175 claims := c.Get("claims").(permission.Claims) 176 slug = claims.Subject 177 } 178 179 // Handles the metadata part 180 md, err := metadata.NewWithApp(slug, "", permission.DocTypeVersion) 181 if err != nil { 182 return err 183 } 184 185 // Adding metadata if it does not exist 186 if subdoc.Metadata == nil { 187 subdoc.Metadata = md 188 } else { // Otherwise, ensure we have all the needed fields 189 subdoc.Metadata.EnsureCreatedFields(md) 190 } 191 192 pdoc, err := permission.CreateShareSet(instance, parent, sourceID, codes, shortcodes, subdoc, expiresAt) 193 if err != nil { 194 return err 195 } 196 197 // Don't send the password hash to the client 198 if pdoc.Password != nil { 199 pdoc.Password = true 200 } 201 202 return jsonapi.Data(c, http.StatusOK, &APIPermission{pdoc, nil}, nil) 203 } 204 205 func createShortCode(tiny bool) string { 206 if tiny { 207 return crypto.GenerateRandomSixDigits() 208 } 209 return crypto.GenerateRandomString(consts.ShortCodeLen) 210 } 211 212 const ( 213 defaultPermissionsByDoctype = 30 214 maxPermissionsByDoctype = 100 215 ) 216 217 func listPermissionsByDoctype(c echo.Context, route, permType string) error { 218 ins := middlewares.GetInstance(c) 219 doctype := c.Param("doctype") 220 if doctype == "" { 221 return jsonapi.NewError(http.StatusBadRequest, "Missing doctype") 222 } 223 224 current, err := middlewares.GetPermission(c) 225 if err != nil { 226 return err 227 } 228 229 if !current.Permissions.AllowWholeType(http.MethodGet, doctype) { 230 return jsonapi.NewError(http.StatusForbidden, 231 "You need GET permission on whole type to list its permissions") 232 } 233 234 cursor, err := jsonapi.ExtractPaginationCursor(c, defaultPermissionsByDoctype, maxPermissionsByDoctype) 235 if err != nil { 236 return err 237 } 238 239 perms, err := permission.GetPermissionsByDoctype(ins, permType, doctype, cursor) 240 if err != nil { 241 return err 242 } 243 244 links := &jsonapi.LinksList{} 245 if cursor.HasMore() { 246 params, err := jsonapi.PaginationCursorToParams(cursor) 247 if err != nil { 248 return err 249 } 250 links.Next = fmt.Sprintf("/permissions/doctype/%s/%s?%s", 251 doctype, route, params.Encode()) 252 } 253 254 out := make([]jsonapi.Object, len(perms)) 255 for i := range perms { 256 perm := &perms[i] 257 if perm.Password != nil { 258 perm.Password = true 259 } 260 out[i] = &APIPermission{perm, nil} 261 } 262 263 return jsonapi.DataList(c, http.StatusOK, out, links) 264 } 265 266 func listByLinkPermissionsByDoctype(c echo.Context) error { 267 return listPermissionsByDoctype(c, "shared-by-link", permission.TypeShareByLink) 268 } 269 270 type refAndVerb struct { 271 ID string `json:"id"` 272 DocType string `json:"type"` 273 Verbs *permission.VerbSet `json:"verbs"` 274 } 275 276 func listPermissions(c echo.Context) error { 277 instance := middlewares.GetInstance(c) 278 279 references, err := jsonapi.BindRelations(c.Request()) 280 if err != nil { 281 return err 282 } 283 ids := make(map[string][]string) 284 for _, ref := range references { 285 idSlice, ok := ids[ref.Type] 286 if !ok { 287 idSlice = []string{} 288 } 289 ids[ref.Type] = append(idSlice, ref.ID) 290 } 291 292 var out []refAndVerb 293 for doctype, idSlice := range ids { 294 result, err2 := permission.GetPermissionsForIDs(instance, doctype, idSlice) 295 if err2 != nil { 296 return err2 297 } 298 for id, verbs := range result { 299 out = append(out, refAndVerb{id, doctype, verbs}) 300 } 301 } 302 303 data, err := json.Marshal(out) 304 if err != nil { 305 return err 306 } 307 doc := jsonapi.Document{ 308 Data: (*json.RawMessage)(&data), 309 } 310 resp := c.Response() 311 resp.Header().Set(echo.HeaderContentType, jsonapi.ContentType) 312 resp.WriteHeader(http.StatusOK) 313 return json.NewEncoder(resp).Encode(doc) 314 } 315 316 func showPermissions(c echo.Context) error { 317 inst := middlewares.GetInstance(c) 318 319 current, err := middlewares.GetPermission(c) 320 if err != nil { 321 return err 322 } 323 324 doc, err := permission.GetByID(inst, c.Param("permdocid")) 325 if err != nil { 326 return err 327 } 328 329 if doc.ID() != current.ID() && doc.SourceID != current.SourceID { 330 if err := middlewares.AllowMaximal(c); err != nil { 331 return middlewares.ErrForbidden 332 } 333 } 334 335 // XXX hides the codes and password hash in the response 336 doc.Codes = nil 337 doc.ShortCodes = nil 338 if doc.Password != nil { 339 doc.Password = true 340 } 341 return jsonapi.Data(c, http.StatusOK, &APIPermission{Permission: doc}, nil) 342 } 343 344 func patchPermission(getPerms getPermsFunc, paramName string) echo.HandlerFunc { 345 return func(c echo.Context) error { 346 instance := middlewares.GetInstance(c) 347 current, err := middlewares.GetPermission(c) 348 if err != nil { 349 return err 350 } 351 352 var patch permission.Permission 353 if _, err = jsonapi.Bind(c.Request().Body, &patch); err != nil { 354 return err 355 } 356 357 patchSet := patch.Permissions != nil && len(patch.Permissions) > 0 358 patchCodes := len(patch.Codes) > 0 359 360 if patchCodes == patchSet { 361 return ErrPatchCodeOrSet 362 } 363 364 toPatch, err := getPerms(instance, c.Param(paramName)) 365 if err != nil { 366 return err 367 } 368 369 if patchCodes { 370 if !current.CanUpdateShareByLink(toPatch) { 371 return permission.ErrNotParent 372 } 373 toPatch.PatchCodes(patch.Codes) 374 } 375 376 if patchSet { 377 for _, r := range patch.Permissions { 378 if r.Type == "" { 379 toPatch.RemoveRule(r) 380 } else if err := permission.CheckDoctypeName(r.Type, true); err != nil { 381 return err 382 } else if current.Permissions.RuleInSubset(r) { 383 toPatch.AddRules(r) 384 } else { 385 return permission.ErrNotSubset 386 } 387 } 388 } 389 390 // Handle metadata 391 // If the metadata has been given in the body request, just apply it to 392 // the patch 393 if patch.Metadata != nil { 394 toPatch.Metadata = patch.Metadata 395 patch.Metadata.EnsureCreatedFields(toPatch.Metadata) 396 } else if toPatch.Metadata != nil { // No metadata given in the request, but it does exist in the database: update it 397 // Using the token Subject for update 398 claims := c.Get("claims").(permission.Claims) 399 err = toPatch.Metadata.UpdatedByApp(claims.Subject, "") 400 if err != nil { 401 return err 402 } 403 } 404 405 if err = couchdb.UpdateDoc(instance, toPatch); err != nil { 406 return err 407 } 408 409 return jsonapi.Data(c, http.StatusOK, &APIPermission{toPatch, nil}, nil) 410 } 411 } 412 413 func revokePermission(c echo.Context) error { 414 instance := middlewares.GetInstance(c) 415 416 current, err := middlewares.GetPermission(c) 417 if err != nil { 418 return err 419 } 420 421 toRevoke, err := permission.GetByID(instance, c.Param("permdocid")) 422 if err != nil { 423 return err 424 } 425 426 if !current.CanUpdateShareByLink(toRevoke) { 427 return permission.ErrNotParent 428 } 429 430 err = toRevoke.Revoke(instance) 431 if err != nil { 432 return err 433 } 434 435 return c.NoContent(http.StatusNoContent) 436 } 437 438 // Routes sets the routing for the permissions service 439 func Routes(router *echo.Group) { 440 // API Routes 441 router.POST("", createPermission) 442 router.GET("/self", displayPermissions) 443 router.POST("/exists", listPermissions) 444 router.GET("/:permdocid", showPermissions) 445 router.PATCH("/:permdocid", patchPermission(permission.GetByID, "permdocid")) 446 router.DELETE("/:permdocid", revokePermission) 447 448 router.PATCH("/apps/:slug", patchPermission(permission.GetForWebapp, "slug")) 449 router.PATCH("/konnectors/:slug", patchPermission(permission.GetForKonnector, "slug")) 450 451 router.GET("/doctype/:doctype/shared-by-link", listByLinkPermissionsByDoctype) 452 453 // Legacy routes, kept here for compatibility reasons 454 router.GET("/doctype/:doctype/sharedByLink", listByLinkPermissionsByDoctype) 455 }