github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/sharings/sharings.go (about) 1 // Package sharings is the HTTP routes for the sharing. We have two types of 2 // routes, some routes are used by the clients to create, list, revoke sharings 3 // and add/remove recipients, and other routes are reserved for an internal 4 // usage, mostly to synchronize the documents between the Cozys of the members 5 // of the sharings. 6 package sharings 7 8 import ( 9 "encoding/json" 10 "errors" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 16 "github.com/cozy/cozy-stack/model/contact" 17 "github.com/cozy/cozy-stack/model/instance" 18 "github.com/cozy/cozy-stack/model/oauth" 19 "github.com/cozy/cozy-stack/model/permission" 20 "github.com/cozy/cozy-stack/model/settings" 21 "github.com/cozy/cozy-stack/model/sharing" 22 "github.com/cozy/cozy-stack/model/vfs" 23 "github.com/cozy/cozy-stack/pkg/avatar" 24 "github.com/cozy/cozy-stack/pkg/config/config" 25 "github.com/cozy/cozy-stack/pkg/consts" 26 "github.com/cozy/cozy-stack/pkg/couchdb" 27 "github.com/cozy/cozy-stack/pkg/crypto" 28 "github.com/cozy/cozy-stack/pkg/jsonapi" 29 "github.com/cozy/cozy-stack/pkg/logger" 30 "github.com/cozy/cozy-stack/pkg/safehttp" 31 "github.com/cozy/cozy-stack/web/middlewares" 32 "github.com/hashicorp/go-multierror" 33 "github.com/labstack/echo/v4" 34 ) 35 36 // CreateSharing initializes a new sharing (on the sharer) 37 func CreateSharing(c echo.Context) error { 38 inst := middlewares.GetInstance(c) 39 40 var s sharing.Sharing 41 obj, err := jsonapi.Bind(c.Request().Body, &s) 42 if err != nil { 43 return jsonapi.BadJSON() 44 } 45 46 slug, err := checkCreatePermissions(c, &s) 47 if err != nil { 48 return echo.NewHTTPError(http.StatusForbidden) 49 } 50 51 if err = s.BeOwner(inst, slug); err != nil { 52 return wrapErrors(err) 53 } 54 55 if rel, ok := obj.GetRelationship("recipients"); ok { 56 if data, ok := rel.Data.([]interface{}); ok { 57 for _, ref := range data { 58 if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups { 59 if id, ok := ref.(map[string]interface{})["id"].(string); ok { 60 if err = s.AddGroup(inst, id, false); err != nil { 61 return err 62 } 63 } 64 } else { 65 if id, ok := ref.(map[string]interface{})["id"].(string); ok { 66 if err = s.AddContact(inst, id, false); err != nil { 67 return err 68 } 69 } 70 } 71 } 72 } 73 } 74 75 if rel, ok := obj.GetRelationship("read_only_recipients"); ok { 76 if data, ok := rel.Data.([]interface{}); ok { 77 for _, ref := range data { 78 if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups { 79 if id, ok := ref.(map[string]interface{})["id"].(string); ok { 80 if err = s.AddGroup(inst, id, true); err != nil { 81 return err 82 } 83 } 84 } else { 85 if id, ok := ref.(map[string]interface{})["id"].(string); ok { 86 if err = s.AddContact(inst, id, true); err != nil { 87 return err 88 } 89 } 90 } 91 } 92 } 93 } 94 95 perms, err := s.Create(inst) 96 if err != nil { 97 return wrapErrors(err) 98 } 99 if err = s.SendInvitations(inst, perms); err != nil { 100 return wrapErrors(err) 101 } 102 as := &sharing.APISharing{ 103 Sharing: &s, 104 Credentials: nil, 105 SharedDocs: nil, 106 } 107 return jsonapi.Data(c, http.StatusCreated, as, nil) 108 } 109 110 // PutSharing creates a sharing request (on the recipient's cozy) 111 func PutSharing(c echo.Context) error { 112 inst := middlewares.GetInstance(c) 113 114 var s sharing.Sharing 115 obj, err := jsonapi.Bind(c.Request().Body, &s) 116 if err != nil { 117 return jsonapi.BadJSON() 118 } 119 s.SID = obj.ID 120 s.ShortcutID = "" 121 122 if err := s.CreateRequest(inst); err != nil { 123 return wrapErrors(err) 124 } 125 126 if c.QueryParam("shortcut") == "true" { 127 u := c.QueryParam("url") 128 if err := s.CreateShortcut(inst, u, false); err != nil { 129 return wrapErrors(err) 130 } 131 } 132 133 as := &sharing.APISharing{ 134 Sharing: &s, 135 Credentials: nil, 136 SharedDocs: nil, 137 } 138 return jsonapi.Data(c, http.StatusCreated, as, nil) 139 } 140 141 // jsonapiSharingWithDocs is an helper to send a JSON-API response for a 142 // sharing with its shared docs 143 func jsonapiSharingWithDocs(c echo.Context, s *sharing.Sharing) error { 144 inst := middlewares.GetInstance(c) 145 sharedDocs, err := sharing.GetSharedDocsBySharingIDs(inst, []string{s.SID}) 146 if err != nil { 147 return wrapErrors(err) 148 } 149 docs := sharedDocs[s.SID] 150 as := &sharing.APISharing{ 151 Sharing: s, 152 Credentials: nil, 153 SharedDocs: docs, 154 } 155 return jsonapi.Data(c, http.StatusOK, as, nil) 156 } 157 158 // GetSharing returns the sharing document associated to the given sharingID 159 // and which documents have been shared. 160 // The requester must have the permission on at least one doctype declared in 161 // the sharing document. 162 func GetSharing(c echo.Context) error { 163 inst := middlewares.GetInstance(c) 164 sharingID := c.Param("sharing-id") 165 s, err := sharing.FindSharing(inst, sharingID) 166 if err != nil { 167 return wrapErrors(err) 168 } 169 if err = checkGetPermissions(c, s); err != nil { 170 return wrapErrors(err) 171 } 172 return jsonapiSharingWithDocs(c, s) 173 } 174 175 // CountNewShortcuts returns the number of shortcuts to a sharing that have not 176 // been seen. 177 func CountNewShortcuts(c echo.Context) error { 178 if _, err := middlewares.GetPermission(c); err != nil { 179 return echo.NewHTTPError(http.StatusForbidden) 180 } 181 182 inst := middlewares.GetInstance(c) 183 count, err := sharing.CountNewShortcuts(inst) 184 if err != nil { 185 return wrapErrors(err) 186 } 187 body := map[string]interface{}{ 188 "meta": map[string]int{ 189 "count": count, 190 }, 191 } 192 return c.JSON(http.StatusOK, body) 193 } 194 195 // GetSharingsInfoByDocType returns, for a given doctype, all the sharing 196 // information, i.e. the involved sharings and the shared documents 197 func GetSharingsInfoByDocType(c echo.Context) error { 198 inst := middlewares.GetInstance(c) 199 docType := c.Param("doctype") 200 201 sharings, err := sharing.GetSharingsByDocType(inst, docType) 202 if err != nil { 203 inst.Logger().WithNamespace("sharing").Errorf("GetSharingsByDocType error: %s", err) 204 return wrapErrors(err) 205 } 206 if err := middlewares.AllowWholeType(c, permission.GET, docType); err != nil { 207 return wrapErrors(err) 208 } 209 if len(sharings) == 0 { 210 return jsonapi.DataList(c, http.StatusOK, nil, nil) 211 } 212 sharingIDs := make([]string, 0, len(sharings)) 213 for sID := range sharings { 214 sharingIDs = append(sharingIDs, sID) 215 } 216 sDocs, err := sharing.GetSharedDocsBySharingIDs(inst, sharingIDs) 217 if err != nil { 218 inst.Logger().WithNamespace("sharing").Errorf("GetSharedDocsBySharingIDs error: %s", err) 219 return wrapErrors(err) 220 } 221 222 res := make([]*sharing.APISharing, 0, len(sharings)) 223 for sID, s := range sharings { 224 as := &sharing.APISharing{ 225 Sharing: s, 226 SharedDocs: sDocs[sID], 227 Credentials: nil, 228 } 229 res = append(res, as) 230 } 231 return sharing.InfoByDocTypeData(c, http.StatusOK, res) 232 } 233 234 // AnswerSharing is used to exchange credentials between 2 cozys, after the 235 // recipient has accepted a sharing. 236 func AnswerSharing(c echo.Context) error { 237 inst := middlewares.GetInstance(c) 238 sharingID := c.Param("sharing-id") 239 s, err := sharing.FindSharing(inst, sharingID) 240 if err != nil { 241 return wrapErrors(err) 242 } 243 var creds sharing.APICredentials 244 if _, err = jsonapi.Bind(c.Request().Body, &creds); err != nil { 245 return jsonapi.BadJSON() 246 } 247 ac, err := s.ProcessAnswer(inst, &creds) 248 if err != nil { 249 return wrapErrors(err) 250 } 251 return jsonapi.Data(c, http.StatusOK, ac, nil) 252 } 253 254 // ReceivePublicKey is used to receive the public key of a sharing member. It can 255 // be used when the member has delegated authentication, and didn't have a 256 // password when they accepted the sharing: this route is called when the user 257 // choose a password a bit later in cozy pass web. 258 func ReceivePublicKey(c echo.Context) error { 259 inst := middlewares.GetInstance(c) 260 sharingID := c.Param("sharing-id") 261 s, err := sharing.FindSharing(inst, sharingID) 262 if err != nil { 263 return wrapErrors(err) 264 } 265 member, err := requestMember(c, s) 266 if err != nil { 267 return wrapErrors(err) 268 } 269 var creds sharing.APICredentials 270 if _, err = jsonapi.Bind(c.Request().Body, &creds); err != nil || creds.Bitwarden == nil { 271 return jsonapi.BadJSON() 272 } 273 if err := s.SaveBitwarden(inst, member, creds.Bitwarden); err != nil { 274 return wrapErrors(err) 275 } 276 return c.NoContent(http.StatusNoContent) 277 } 278 279 // ChangeCozyAddress is called when a Cozy has been moved to a new address. 280 func ChangeCozyAddress(c echo.Context) error { 281 inst := middlewares.GetInstance(c) 282 sharingID := c.Param("sharing-id") 283 s, err := sharing.FindSharing(inst, sharingID) 284 if err != nil { 285 return wrapErrors(err) 286 } 287 var moved sharing.APIMoved 288 if _, err = jsonapi.Bind(c.Request().Body, &moved); err != nil { 289 return jsonapi.BadJSON() 290 } 291 292 member, err := requestMember(c, s) 293 if err != nil { 294 return wrapErrors(err) 295 } 296 297 if s.Owner { 298 err = s.ChangeMemberAddress(inst, member, moved) 299 } else { 300 err = s.ChangeOwnerAddress(inst, moved) 301 } 302 if err != nil { 303 return wrapErrors(err) 304 } 305 return c.NoContent(http.StatusNoContent) 306 } 307 308 func addRecipientsToSharing(inst *instance.Instance, s *sharing.Sharing, rel *jsonapi.Relationship, readOnly bool) error { 309 var err error 310 if data, ok := rel.Data.([]interface{}); ok { 311 var contactIDs, groupIDs []string 312 for _, ref := range data { 313 if id, ok := ref.(map[string]interface{})["id"].(string); ok { 314 if t, _ := ref.(map[string]interface{})["type"].(string); t == consts.Groups { 315 groupIDs = append(groupIDs, id) 316 } else { 317 contactIDs = append(contactIDs, id) 318 } 319 } 320 } 321 if s.Owner { 322 err = s.AddGroupsAndContacts(inst, groupIDs, contactIDs, readOnly) 323 } else { 324 err = s.DelegateAddContactsAndGroups(inst, groupIDs, contactIDs, readOnly) 325 } 326 } 327 return err 328 } 329 330 // AddRecipients is used to add a member to a sharing 331 func AddRecipients(c echo.Context) error { 332 inst := middlewares.GetInstance(c) 333 sharingID := c.Param("sharing-id") 334 s, err := sharing.FindSharing(inst, sharingID) 335 if err != nil { 336 return wrapErrors(err) 337 } 338 if _, err = checkCreatePermissions(c, s); err != nil { 339 return wrapErrors(err) 340 } 341 var body sharing.Sharing 342 obj, err := jsonapi.Bind(c.Request().Body, &body) 343 if err != nil { 344 return jsonapi.BadJSON() 345 } 346 if rel, ok := obj.GetRelationship("recipients"); ok { 347 if err = addRecipientsToSharing(inst, s, rel, false); err != nil { 348 return wrapErrors(err) 349 } 350 } 351 if rel, ok := obj.GetRelationship("read_only_recipients"); ok { 352 if err = addRecipientsToSharing(inst, s, rel, true); err != nil { 353 return wrapErrors(err) 354 } 355 } 356 return jsonapiSharingWithDocs(c, s) 357 } 358 359 // AddRecipientsDelegated is used to add members and groups to a sharing on the 360 // owner's cozy when it's the recipient's cozy that sends the mail invitation. 361 func AddRecipientsDelegated(c echo.Context) error { 362 inst := middlewares.GetInstance(c) 363 sharingID := c.Param("sharing-id") 364 s, err := sharing.FindSharing(inst, sharingID) 365 if err != nil { 366 return wrapErrors(err) 367 } 368 if !s.Owner || !s.Open { 369 return echo.NewHTTPError(http.StatusForbidden) 370 } 371 member, err := requestMember(c, s) 372 if err != nil { 373 return wrapErrors(err) 374 } 375 memberIndex := -1 376 for i, m := range s.Members { 377 if m.Instance == member.Instance { 378 memberIndex = i 379 } 380 } 381 if memberIndex == -1 { 382 return jsonapi.InternalServerError(sharing.ErrInvalidSharing) 383 } 384 385 var body struct { 386 Data struct { 387 Type string `json:"type"` 388 ID string `json:"id"` 389 Relationships struct { 390 Groups struct { 391 Data []sharing.Group `json:"data"` 392 } `json:"groups"` 393 Recipients struct { 394 Data []sharing.Member `json:"data"` 395 } `json:"recipients"` 396 } `json:"relationships"` 397 } `json:"data"` 398 } 399 if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil { 400 return jsonapi.BadJSON() 401 } 402 403 for _, g := range body.Data.Relationships.Groups.Data { 404 g.AddedBy = memberIndex 405 s.Groups = append(s.Groups, g) 406 } 407 408 states := make(map[string]string) 409 for _, m := range body.Data.Relationships.Recipients.Data { 410 state, err := s.AddDelegatedContact(inst, m) 411 if err != nil { 412 if len(m.Groups) > 0 { 413 continue 414 } 415 return wrapErrors(err) 416 } 417 // If we have an URL for the Cozy, we can create a shortcut as an invitation 418 if m.Instance != "" { 419 states[m.Instance] = state 420 var perms *permission.Permission 421 if s.PreviewPath != "" { 422 if perms, err = s.CreatePreviewPermissions(inst); err != nil { 423 return wrapErrors(err) 424 } 425 } 426 if err = s.SendInvitations(inst, perms); err != nil { 427 return wrapErrors(err) 428 } 429 } else if m.Email != "" { 430 states[m.Email] = state 431 } 432 } 433 434 if err := couchdb.UpdateDoc(inst, s); err != nil { 435 return wrapErrors(err) 436 } 437 cloned := s.Clone().(*sharing.Sharing) 438 go cloned.NotifyRecipients(inst, nil) 439 return c.JSON(http.StatusOK, states) 440 } 441 442 // AddInvitationDelegated is when a member has been added to a sharing via a 443 // group, but is invited only later (no email or Cozy instance known when they 444 // was added). 445 func AddInvitationDelegated(c echo.Context) error { 446 inst := middlewares.GetInstance(c) 447 sharingID := c.Param("sharing-id") 448 s, err := sharing.FindSharing(inst, sharingID) 449 if err != nil { 450 return wrapErrors(err) 451 } 452 if !s.Owner || !s.Open { 453 return echo.NewHTTPError(http.StatusForbidden) 454 } 455 456 memberIndex, err := strconv.Atoi(c.Param("member-index")) 457 if err != nil || memberIndex <= 0 || memberIndex >= len(s.Members) { 458 return jsonapi.InvalidParameter("member-index", errors.New("invalid member-index parameter")) 459 } 460 461 var body struct { 462 Data struct { 463 Type string `json:"type"` 464 Member sharing.Member `json:"attributes"` 465 } 466 } 467 if err = json.NewDecoder(c.Request().Body).Decode(&body); err != nil { 468 return jsonapi.BadJSON() 469 } 470 471 states := make(map[string]string) 472 m := s.Members[memberIndex] 473 if m.Status == sharing.MemberStatusMailNotSent { 474 m.Instance = body.Data.Member.Instance 475 m.Email = body.Data.Member.Email 476 state64 := crypto.Base64Encode(crypto.GenerateRandomBytes(sharing.StateLen)) 477 state := string(state64) 478 creds := sharing.Credentials{ 479 State: state, 480 XorKey: sharing.MakeXorKey(), 481 } 482 s.Credentials[memberIndex-1] = creds 483 s.Members[memberIndex] = m 484 // If we have an URL for the Cozy, we can create a shortcut as an invitation 485 if m.Instance != "" { 486 states[m.Instance] = state 487 var perms *permission.Permission 488 if s.PreviewPath != "" { 489 if perms, err = s.CreatePreviewPermissions(inst); err != nil { 490 return wrapErrors(err) 491 } 492 } 493 if err = s.SendInvitations(inst, perms); err != nil { 494 return wrapErrors(err) 495 } 496 } else if m.Email != "" { 497 states[m.Email] = state 498 s.Members[memberIndex].Status = sharing.MemberStatusReady 499 } 500 } 501 502 if err := couchdb.UpdateDoc(inst, s); err != nil { 503 return wrapErrors(err) 504 } 505 cloned := s.Clone().(*sharing.Sharing) 506 go cloned.NotifyRecipients(inst, nil) 507 return c.JSON(http.StatusOK, states) 508 } 509 510 // RemoveMemberFromGroup is used to remove a member from a group (delegated). 511 func RemoveMemberFromGroup(c echo.Context) error { 512 inst := middlewares.GetInstance(c) 513 sharingID := c.Param("sharing-id") 514 s, err := sharing.FindSharing(inst, sharingID) 515 if err != nil { 516 return wrapErrors(err) 517 } 518 if !s.Owner || !s.Open { 519 return echo.NewHTTPError(http.StatusForbidden) 520 } 521 522 member, err := requestMember(c, s) 523 if err != nil { 524 return wrapErrors(err) 525 } 526 addedBy := -1 527 for i, m := range s.Members { 528 if m.Instance == member.Instance { 529 addedBy = i 530 } 531 } 532 if addedBy == -1 { 533 return jsonapi.InternalServerError(sharing.ErrInvalidSharing) 534 } 535 536 groupIndex, err := strconv.Atoi(c.Param("group-index")) 537 if err != nil || groupIndex < 0 || groupIndex >= len(s.Groups) { 538 return jsonapi.InvalidParameter("group-index", errors.New("invalid group-index parameter")) 539 } 540 if s.Groups[groupIndex].AddedBy != addedBy { 541 return echo.NewHTTPError(http.StatusForbidden) 542 } 543 544 memberIndex, err := strconv.Atoi(c.Param("member-index")) 545 if err != nil || memberIndex <= 0 || memberIndex >= len(s.Members) { 546 return jsonapi.InvalidParameter("member-index", errors.New("invalid member-index parameter")) 547 } 548 549 if err := s.DelegatedRemoveMemberFromGroup(inst, groupIndex, memberIndex); err != nil { 550 return wrapErrors(err) 551 } 552 return c.NoContent(http.StatusNoContent) 553 } 554 555 // PutRecipients is used to update the members list on the recipients cozy 556 func PutRecipients(c echo.Context) error { 557 inst := middlewares.GetInstance(c) 558 sharingID := c.Param("sharing-id") 559 s, err := sharing.FindSharing(inst, sharingID) 560 if err != nil { 561 return wrapErrors(err) 562 } 563 564 if s.Active { 565 // If the sharing is active, we check the access token for a permission 566 // on the sharing 567 if err := hasSharingWritePermissions(c); err != nil { 568 return err 569 } 570 } else { 571 // If there is no synchronization, it means that we have a shortcut for 572 // this sharing, and we can check the sharecode. 573 token := middlewares.GetRequestToken(c) 574 sharecode, err := s.GetSharecodeFromShortcut(inst) 575 if err != nil || token != sharecode { 576 return middlewares.ErrForbidden 577 } 578 } 579 580 var params sharing.PutRecipientsParams 581 if err = json.NewDecoder(c.Request().Body).Decode(¶ms); err != nil { 582 return wrapErrors(err) 583 } 584 if err = s.UpdateRecipients(inst, params); err != nil { 585 return wrapErrors(err) 586 } 587 return c.NoContent(http.StatusNoContent) 588 } 589 590 func renderAlreadyAccepted(c echo.Context, inst *instance.Instance, cozyURL string) error { 591 return c.Render(http.StatusBadRequest, "error.html", echo.Map{ 592 "Domain": inst.ContextualDomain(), 593 "ContextName": inst.ContextName, 594 "Locale": inst.Locale, 595 "Title": inst.TemplateTitle(), 596 "Favicon": middlewares.Favicon(inst), 597 "ErrorTitle": "Error Sharing already accepted Title", 598 "Error": "Error Sharing already accepted", 599 "Button": "Error Sharing already accepted Button", 600 "ButtonURL": cozyURL, 601 "SupportEmail": inst.SupportEmailAddress(), 602 }) 603 } 604 605 func renderDiscoveryForm(c echo.Context, inst *instance.Instance, code int, sharingID, state, sharecode string, m *sharing.Member) error { 606 publicName, _ := settings.PublicName(inst) 607 fqdn := strings.TrimPrefix(m.Instance, "https://") 608 slug, domain := "", consts.KnownFlatDomains[0] 609 if context, ok := inst.SettingsContext(); ok { 610 if d, ok := context["sharing_domain"].(string); ok { 611 domain = d 612 } 613 } 614 if strings.HasPrefix(m.Instance, "http://") { 615 slug, domain = m.Instance, "" 616 } else if parts := strings.SplitN(fqdn, ".", 2); len(parts) == 2 { 617 slug, domain = parts[0], parts[1] 618 } 619 return c.Render(code, "sharing_discovery.html", echo.Map{ 620 "Domain": inst.ContextualDomain(), 621 "ContextName": inst.ContextName, 622 "Locale": inst.Locale, 623 "Title": inst.TemplateTitle(), 624 "Favicon": middlewares.Favicon(inst), 625 "PublicName": publicName, 626 "RecipientSlug": slug, 627 "RecipientDomain": domain, 628 "SharingID": sharingID, 629 "State": state, 630 "ShareCode": sharecode, 631 "URLError": code == http.StatusBadRequest, 632 "NotEmailError": code == http.StatusPreconditionFailed, 633 }) 634 } 635 636 // GetDiscovery displays a form where a recipient can give the address of their 637 // cozy instance 638 func GetDiscovery(c echo.Context) error { 639 inst := middlewares.GetInstance(c) 640 sharingID := c.Param("sharing-id") 641 state := c.QueryParam("state") 642 sharecode := c.FormValue("sharecode") 643 644 s, err := sharing.FindSharing(inst, sharingID) 645 if err != nil { 646 return c.Render(http.StatusBadRequest, "error.html", echo.Map{ 647 "Domain": inst.ContextualDomain(), 648 "ContextName": inst.ContextName, 649 "Locale": inst.Locale, 650 "Title": inst.TemplateTitle(), 651 "Favicon": middlewares.Favicon(inst), 652 "Illustration": "/images/generic-error.svg", 653 "Error": "Error Invalid sharing", 654 "SupportEmail": inst.SupportEmailAddress(), 655 }) 656 } 657 658 m := &sharing.Member{} 659 if s.Owner { 660 if sharecode != "" { 661 m, err = s.FindMemberBySharecode(inst, sharecode) 662 } else { 663 m, err = s.FindMemberByState(state) 664 } 665 if err != nil || m.Status == sharing.MemberStatusRevoked { 666 return c.Render(http.StatusBadRequest, "error.html", echo.Map{ 667 "Domain": inst.ContextualDomain(), 668 "ContextName": inst.ContextName, 669 "Locale": inst.Locale, 670 "Title": inst.TemplateTitle(), 671 "Favicon": middlewares.Favicon(inst), 672 "Illustration": "/images/generic-error.svg", 673 "Error": "Error Invalid sharing", 674 "SupportEmail": inst.SupportEmailAddress(), 675 }) 676 } 677 if m.Status != sharing.MemberStatusMailNotSent && 678 m.Status != sharing.MemberStatusPendingInvitation && 679 m.Status != sharing.MemberStatusSeen { 680 return renderAlreadyAccepted(c, inst, m.Instance) 681 } 682 } 683 684 if m.Instance != "" { 685 if m.Status != sharing.MemberStatusSeen { 686 err = s.RegisterCozyURL(inst, m, m.Instance) 687 } 688 if err == nil { 689 redirectURL, err := m.GenerateOAuthURL(s) 690 if err == nil { 691 return c.Redirect(http.StatusFound, redirectURL) 692 } 693 } 694 } 695 696 return renderDiscoveryForm(c, inst, http.StatusOK, sharingID, state, sharecode, m) 697 } 698 699 // PostDiscovery is called when the recipient has given its Cozy URL. Either an 700 // error is returned or the recipient will be redirected to their cozy. 701 // 702 // Note: we don't have an anti-CSRF system, we rely on shareCode being secret. 703 func PostDiscovery(c echo.Context) error { 704 inst := middlewares.GetInstance(c) 705 sharingID := c.Param("sharing-id") 706 state := c.FormValue("state") 707 sharecode := c.FormValue("sharecode") 708 cozyURL := c.FormValue("url") 709 if cozyURL == "" { 710 cozyURL = c.FormValue("slug") 711 } 712 cozyURL = strings.TrimSuffix(cozyURL, ".") 713 if !strings.HasPrefix(cozyURL, "http://") && !strings.HasPrefix(cozyURL, "https://") { 714 cozyURL = "https://" + cozyURL 715 } 716 if domain := c.FormValue("domain"); domain != "" && !strings.Contains(cozyURL, ".") { 717 if domain == "mycosy.cloud" { 718 domain = "mycozy.cloud" 719 } 720 cozyURL = cozyURL + "." + domain 721 } 722 cozyURL = ClearAppInURL(cozyURL) 723 724 s, err := sharing.FindSharing(inst, sharingID) 725 if err != nil { 726 return wrapErrors(err) 727 } 728 729 var redirectURL, email string 730 731 if s.Owner { 732 var member *sharing.Member 733 if sharecode != "" { 734 member, err = s.FindMemberBySharecode(inst, sharecode) 735 if err != nil { 736 return wrapErrors(err) 737 } 738 } else { 739 member, err = s.FindMemberByState(state) 740 if err != nil { 741 return wrapErrors(err) 742 } 743 } 744 if strings.Contains(cozyURL, "@") { 745 return renderDiscoveryForm(c, inst, http.StatusPreconditionFailed, sharingID, state, sharecode, member) 746 } 747 email = member.Email 748 if err = s.RegisterCozyURL(inst, member, cozyURL); err != nil { 749 if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON { 750 return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()}) 751 } 752 if errors.Is(err, sharing.ErrAlreadyAccepted) { 753 return renderAlreadyAccepted(c, inst, cozyURL) 754 } 755 return renderDiscoveryForm(c, inst, http.StatusBadRequest, sharingID, state, sharecode, member) 756 } 757 redirectURL, err = member.GenerateOAuthURL(s) 758 if err != nil { 759 return wrapErrors(err) 760 } 761 sharing.PersistInstanceURL(inst, member.Email, member.Instance) 762 } else { 763 redirectURL, err = s.DelegateDiscovery(inst, state, cozyURL) 764 if err != nil { 765 if errors.Is(err, sharing.ErrInvalidURL) { 766 if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON { 767 return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()}) 768 } 769 return renderDiscoveryForm(c, inst, http.StatusBadRequest, sharingID, state, sharecode, &sharing.Member{}) 770 } 771 return wrapErrors(err) 772 } 773 } 774 775 if c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON { 776 m := echo.Map{"redirect": redirectURL} 777 if email != "" { 778 m["email"] = email 779 } 780 return c.JSON(http.StatusOK, m) 781 } 782 return c.Redirect(http.StatusFound, redirectURL) 783 } 784 785 // GetPreviewURL returns the preview URL for the member identified by their 786 // state parameter. 787 func GetPreviewURL(c echo.Context) error { 788 inst := middlewares.GetInstance(c) 789 s, err := sharing.FindSharing(inst, c.Param("sharing-id")) 790 if err != nil { 791 return wrapErrors(err) 792 } 793 if !s.Owner { 794 return wrapErrors(sharing.ErrInvalidSharing) 795 } 796 797 args := struct { 798 State string `json:"state"` 799 }{} 800 if err := c.Bind(&args); err != nil { 801 return wrapErrors(err) 802 } 803 if args.State == "" { 804 return jsonapi.BadJSON() 805 } 806 m, err := s.FindMemberByState(args.State) 807 if err != nil { 808 return wrapErrors(err) 809 } 810 811 if m.Status != sharing.MemberStatusMailNotSent && 812 m.Status != sharing.MemberStatusPendingInvitation && 813 m.Status != sharing.MemberStatusSeen { 814 return wrapErrors(sharing.ErrAlreadyAccepted) 815 } 816 817 perm, err := permission.GetForSharePreview(inst, s.SID) 818 if err != nil { 819 return wrapErrors(err) 820 } 821 previewURL := m.InvitationLink(inst, s, args.State, perm) 822 return c.JSON(http.StatusOK, map[string]string{"url": previewURL}) 823 } 824 825 // GetAvatar returns the avatar of the given member of the sharing. 826 func GetAvatar(c echo.Context) error { 827 inst := middlewares.GetInstance(c) 828 sharingID := c.Param("sharing-id") 829 s, err := sharing.FindSharing(inst, sharingID) 830 if err != nil { 831 return wrapErrors(err) 832 } 833 834 index, err := strconv.Atoi(c.Param("index")) 835 if err != nil { 836 return jsonapi.InvalidParameter("index", err) 837 } 838 if index > len(s.Members) { 839 return jsonapi.NotFound(errors.New("member not found")) 840 } 841 m := s.Members[index] 842 843 // Use the local avatar 844 if m.Instance == "" || m.Instance == inst.PageURL("", nil) { 845 return localAvatar(c, m) 846 } 847 848 // Use the public avatar from the member's instance 849 res, err := safehttp.DefaultClient.Get(m.Instance + "/public/avatar?fallback=404") 850 if err != nil { 851 return localAvatar(c, m) 852 } 853 defer res.Body.Close() 854 if res.StatusCode == http.StatusNotFound && c.QueryParam("fallback") != "404" { 855 return localAvatar(c, m) 856 } 857 return c.Stream(res.StatusCode, res.Header.Get(echo.HeaderContentType), res.Body) 858 } 859 860 func localAvatar(c echo.Context, m sharing.Member) error { 861 name := m.PublicName 862 if name == "" { 863 name = strings.Split(m.Email, "@")[0] 864 } 865 name = strings.ToUpper(name) 866 var options []avatar.Options 867 if m.Status == sharing.MemberStatusMailNotSent || 868 m.Status == sharing.MemberStatusPendingInvitation { 869 options = append(options, avatar.GreyBackground) 870 } 871 img, mime, err := config.Avatars().GenerateInitials(name, options...) 872 if err != nil { 873 return wrapErrors(err) 874 } 875 return c.Blob(http.StatusOK, mime, img) 876 } 877 878 // Routes sets the routing for the sharing service 879 func Routes(router *echo.Group) { 880 // Create a sharing 881 router.POST("/", CreateSharing) // On the sharer 882 router.PUT("/:sharing-id", PutSharing) // On a recipient 883 router.GET("/:sharing-id", GetSharing) 884 router.POST("/:sharing-id/answer", AnswerSharing) 885 886 // Managing recipients 887 router.POST("/:sharing-id/recipients", AddRecipients) 888 router.PUT("/:sharing-id/recipients", PutRecipients) 889 router.DELETE("/:sharing-id/recipients", RevokeSharing) // On the sharer 890 router.DELETE("/:sharing-id/recipients/:index", RevokeRecipient) // On the sharer 891 router.DELETE("/:sharing-id/groups/:index", RevokeGroup) // On the sharer 892 router.POST("/:sharing-id/recipients/self/moved", ChangeCozyAddress) 893 router.POST("/:sharing-id/recipients/:index/readonly", AddReadOnly) // On the sharer 894 router.POST("/:sharing-id/recipients/self/readonly", DowngradeToReadOnly, checkSharingWritePermissions) // On the recipient 895 router.DELETE("/:sharing-id/recipients/:index/readonly", RemoveReadOnly) // On the sharer 896 router.DELETE("/:sharing-id/recipients/self/readonly", UpgradeToReadWrite, checkSharingWritePermissions) // On the recipient 897 router.DELETE("/:sharing-id", RevocationRecipientNotif, checkSharingWritePermissions) // On the recipient 898 router.DELETE("/:sharing-id/recipients/self", RevokeRecipientBySelf) // On the recipient 899 router.DELETE("/:sharing-id/answer", RevocationOwnerNotif, checkSharingWritePermissions) // On the sharer 900 router.POST("/:sharing-id/public-key", ReceivePublicKey) 901 902 // Delegated routes for open sharing 903 router.POST("/:sharing-id/recipients/delegated", AddRecipientsDelegated, checkSharingWritePermissions) 904 router.POST("/:sharing-id/members/:index/invitation", AddInvitationDelegated, checkSharingWritePermissions) 905 router.DELETE("/:sharing-id/groups/:group-index/:member-index", RemoveMemberFromGroup, checkSharingWritePermissions) 906 907 // Misc 908 router.GET("/news", CountNewShortcuts) 909 router.GET("/doctype/:doctype", GetSharingsInfoByDocType) 910 router.GET("/:sharing-id/recipients/:index/avatar", GetAvatar) 911 912 // Register the URL of their Cozy for recipients 913 router.GET("/:sharing-id/discovery", GetDiscovery) 914 router.POST("/:sharing-id/discovery", PostDiscovery) 915 router.POST("/:sharing-id/preview-url", GetPreviewURL) 916 917 // Replicator routes 918 replicatorRoutes(router) 919 } 920 921 func extractSlugFromSourceID(sourceID string) (string, error) { 922 parts := strings.SplitN(sourceID, "/", 2) 923 if len(parts) < 2 { 924 return "", jsonapi.BadRequest(errors.New("Invalid request")) 925 } 926 slug := parts[1] 927 return slug, nil 928 } 929 930 // checkCreatePermissions checks the sharer's token has all the permissions 931 // matching the ones defined in the sharing document 932 func checkCreatePermissions(c echo.Context, s *sharing.Sharing) (string, error) { 933 requestPerm, err := middlewares.GetPermission(c) 934 if err != nil { 935 return "", err 936 } 937 if requestPerm.Type != permission.TypeWebapp && 938 requestPerm.Type != permission.TypeOauth && 939 requestPerm.Type != permission.TypeCLI { 940 return "", permission.ErrInvalidAudience 941 } 942 for _, r := range s.Rules { 943 pr := permission.Rule{ 944 Title: r.Title, 945 Type: r.DocType, 946 Verbs: permission.ALL, 947 Selector: r.Selector, 948 Values: r.Values, 949 } 950 if !requestPerm.Permissions.RuleInSubset(pr) { 951 return "", echo.NewHTTPError(http.StatusForbidden) 952 } 953 } 954 if requestPerm.Type == permission.TypeCLI { 955 return "", nil 956 } 957 if requestPerm.Type == permission.TypeOauth { 958 if requestPerm.Client != nil { 959 oauthClient := requestPerm.Client.(*oauth.Client) 960 if slug := oauth.GetLinkedAppSlug(oauthClient.SoftwareID); slug != "" { 961 return slug, nil 962 } 963 } 964 return "", nil 965 } 966 return extractSlugFromSourceID(requestPerm.SourceID) 967 } 968 969 // checkGetPermissions checks the requester's token has at least one doctype 970 // permission declared in the rules of the sharing document 971 func checkGetPermissions(c echo.Context, s *sharing.Sharing) error { 972 requestPerm, err := middlewares.GetPermission(c) 973 if err != nil { 974 return err 975 } 976 977 if requestPerm.SourceID == consts.Sharings+"/"+s.SID { 978 if requestPerm.Type == permission.TypeSharePreview || 979 requestPerm.Type == permission.TypeShareInteract { 980 return nil 981 } 982 } 983 if requestPerm.Type != permission.TypeWebapp && 984 requestPerm.Type != permission.TypeOauth && 985 requestPerm.Type != permission.TypeCLI { 986 return permission.ErrInvalidAudience 987 } 988 989 for _, r := range s.Rules { 990 pr := permission.Rule{ 991 Title: r.Title, 992 Type: r.DocType, 993 Verbs: permission.Verbs(permission.GET), 994 Selector: r.Selector, 995 Values: r.Values, 996 } 997 if requestPerm.Permissions.RuleInSubset(pr) { 998 return nil 999 } 1000 } 1001 return echo.NewHTTPError(http.StatusForbidden) 1002 } 1003 1004 // ClearAppInURL will remove the app slug from the URL of a Cozy. 1005 // Example: https://john-drive.mycozy.cloud/ -> https://john.mycozy.cloud/ 1006 func ClearAppInURL(cozyURL string) string { 1007 u, err := url.Parse(cozyURL) 1008 if err != nil { 1009 return cozyURL 1010 } 1011 knownDomain := false 1012 for _, domain := range consts.KnownFlatDomains { 1013 if strings.HasSuffix(u.Host, domain) { 1014 knownDomain = true 1015 break 1016 } 1017 } 1018 if !knownDomain { 1019 return cozyURL 1020 } 1021 parts := strings.SplitN(u.Host, ".", 2) 1022 sub := parts[0] 1023 domain := parts[1] 1024 parts = strings.SplitN(sub, "-", 2) 1025 u.Host = parts[0] + "." + domain 1026 return u.String() 1027 } 1028 1029 // wrapErrors returns a formatted error 1030 func wrapErrors(err error) error { 1031 if merr, ok := err.(*multierror.Error); ok { 1032 err = merr.WrappedErrors()[0] 1033 } 1034 switch err { 1035 case contact.ErrNoMailAddress: 1036 return jsonapi.InvalidAttribute("recipients", err) 1037 case sharing.ErrNoRecipients, sharing.ErrNoRules: 1038 return jsonapi.BadRequest(err) 1039 case sharing.ErrTooManyMembers: 1040 return jsonapi.BadRequest(err) 1041 case sharing.ErrInvalidURL: 1042 return jsonapi.InvalidParameter("url", err) 1043 case sharing.ErrInvalidSharing, sharing.ErrInvalidRule: 1044 return jsonapi.BadRequest(err) 1045 case sharing.ErrMemberNotFound: 1046 return jsonapi.NotFound(err) 1047 case sharing.ErrInvitationNotSent: 1048 return jsonapi.BadRequest(err) 1049 case sharing.ErrRequestFailed: 1050 return jsonapi.BadGateway(err) 1051 case sharing.ErrNoOAuthClient: 1052 return jsonapi.BadRequest(err) 1053 case sharing.ErrMissingID, sharing.ErrMissingRev: 1054 return jsonapi.BadRequest(err) 1055 case sharing.ErrInternalServerError: 1056 return jsonapi.InternalServerError(err) 1057 case sharing.ErrMissingFileMetadata: 1058 return jsonapi.NotFound(err) 1059 case sharing.ErrFolderNotFound: 1060 return jsonapi.NotFound(err) 1061 case sharing.ErrSafety: 1062 return jsonapi.BadRequest(err) 1063 case sharing.ErrAlreadyAccepted: 1064 return jsonapi.Conflict(err) 1065 case vfs.ErrInvalidHash: 1066 return jsonapi.InvalidParameter("md5sum", err) 1067 case vfs.ErrContentLengthMismatch: 1068 return jsonapi.PreconditionFailed("Content-Length", err) 1069 case vfs.ErrConflict: 1070 return jsonapi.Conflict(err) 1071 case vfs.ErrFileTooBig, vfs.ErrMaxFileSize: 1072 return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err) 1073 case permission.ErrExpiredToken: 1074 return jsonapi.BadRequest(err) 1075 case sharing.ErrGroupCannotBeAddedTwice, sharing.ErrMemberAlreadyAdded, sharing.ErrMemberAlreadyInGroup: 1076 return jsonapi.BadRequest(err) 1077 } 1078 logger.WithNamespace("sharing").Warnf("Not wrapped error: %s", err) 1079 return err 1080 }