github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/sharing/member.go (about) 1 package sharing 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "runtime" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/cozy/cozy-stack/client/auth" 15 "github.com/cozy/cozy-stack/client/request" 16 "github.com/cozy/cozy-stack/model/bitwarden" 17 "github.com/cozy/cozy-stack/model/contact" 18 "github.com/cozy/cozy-stack/model/instance" 19 "github.com/cozy/cozy-stack/model/job" 20 "github.com/cozy/cozy-stack/model/permission" 21 csettings "github.com/cozy/cozy-stack/model/settings" 22 "github.com/cozy/cozy-stack/pkg/consts" 23 "github.com/cozy/cozy-stack/pkg/couchdb" 24 "github.com/cozy/cozy-stack/pkg/crypto" 25 "github.com/cozy/cozy-stack/pkg/jsonapi" 26 "github.com/cozy/cozy-stack/pkg/mail" 27 "github.com/cozy/cozy-stack/pkg/metadata" 28 "github.com/cozy/cozy-stack/pkg/prefixer" 29 "github.com/labstack/echo/v4" 30 ) 31 32 const ( 33 // MemberStatusOwner is the status for the member that is owner 34 MemberStatusOwner = "owner" 35 // MemberStatusMailNotSent is the initial status for a recipient, before 36 // the mail invitation is sent 37 MemberStatusMailNotSent = "mail-not-sent" 38 // MemberStatusPendingInvitation is for a recipient that has not (yet) 39 // seen the preview of the sharing, but the invitation mail was sent 40 MemberStatusPendingInvitation = "pending" 41 // MemberStatusSeen is for a recipient that has seen the preview of the 42 // sharing, but not accepted it (yet) 43 MemberStatusSeen = "seen" 44 // MemberStatusReady is for recipient that have accepted the sharing 45 MemberStatusReady = "ready" 46 // MemberStatusRevoked is for a revoked member 47 MemberStatusRevoked = "revoked" 48 ) 49 50 const maximalNumberOfMembers = 90 51 52 func maxNumberOfMembers(inst *instance.Instance) int { 53 if settings, ok := inst.SettingsContext(); ok { 54 if max, ok := settings["max_members_per_sharing"].(float64); ok { 55 return int(max) 56 } 57 } 58 return maximalNumberOfMembers 59 } 60 61 // Member contains the information about a recipient (or the sharer) for a sharing 62 type Member struct { 63 Status string `json:"status"` 64 Name string `json:"name,omitempty"` 65 PublicName string `json:"public_name,omitempty"` 66 Email string `json:"email,omitempty"` 67 Instance string `json:"instance,omitempty"` 68 ReadOnly bool `json:"read_only,omitempty"` 69 OnlyInGroups bool `json:"only_in_groups,omitempty"` // False if the member has been added as an io.cozy.contacts 70 Groups []int `json:"groups,omitempty"` // The indexes of the groups a member is part of 71 } 72 73 // PrimaryName returns the main name of this member 74 func (m *Member) PrimaryName() string { 75 if m.Name != "" { 76 return m.Name 77 } 78 if m.PublicName != "" { 79 return m.PublicName 80 } 81 return m.Email 82 } 83 84 // InstanceHost returns the domain part of the Cozy URL of the member, which 85 // can be used to find the instance in CouchDB. It may includes the port. 86 func (m *Member) InstanceHost() string { 87 u, err := url.Parse(m.Instance) 88 if err != nil { 89 return "" 90 } 91 return u.Host 92 } 93 94 // Same returns true if the two members are the same. 95 func (m *Member) Same(other Member) bool { 96 return m.Name == other.Name && 97 m.PublicName == other.PublicName && 98 m.Email == other.Email && 99 m.Instance == other.Instance 100 } 101 102 // Credentials is the struct with the secret stuff used for authentication & 103 // authorization. 104 type Credentials struct { 105 // OAuth state to accept the sharing (authorize phase) 106 State string `json:"state,omitempty"` 107 108 // Information needed to send data to the member 109 Client *auth.Client `json:"client,omitempty"` 110 AccessToken *auth.AccessToken `json:"access_token,omitempty"` 111 112 // XorKey is used to transform file identifiers 113 XorKey []byte `json:"xor_key,omitempty"` 114 115 // InboundClientID is the OAuth ClientID used for authentifying incoming 116 // requests from the member 117 InboundClientID string `json:"inbound_client_id,omitempty"` 118 } 119 120 // AddGroupsAndContacts adds a list of contacts on the sharer cozy 121 func (s *Sharing) AddGroupsAndContacts(inst *instance.Instance, groupIDs, contactIDs []string, readOnly bool) error { 122 for _, id := range contactIDs { 123 if err := s.AddContact(inst, id, readOnly); err != nil { 124 return err 125 } 126 } 127 for _, id := range groupIDs { 128 if err := s.AddGroup(inst, id, readOnly); err != nil { 129 return err 130 } 131 } 132 var err error 133 var perms *permission.Permission 134 if s.PreviewPath != "" { 135 if perms, err = s.CreatePreviewPermissions(inst); err != nil { 136 return err 137 } 138 } 139 _ = couchdb.UpdateDoc(inst, s) 140 if err = s.SendInvitations(inst, perms); err != nil { 141 return err 142 } 143 cloned := s.Clone().(*Sharing) 144 go cloned.NotifyRecipients(inst, nil) 145 return nil 146 } 147 148 // AddContact adds the contact with the given identifier 149 func (s *Sharing) AddContact(inst *instance.Instance, contactID string, readOnly bool) error { 150 c, err := contact.Find(inst, contactID) 151 if err != nil { 152 return err 153 } 154 m := buildMemberFromContact(c, readOnly) 155 if m.Email == "" && m.Instance == "" { 156 return contact.ErrNoMailAddress 157 } 158 _, _, err = s.addMember(inst, m) 159 return err 160 } 161 162 func buildMemberFromContact(c *contact.Contact, readOnly bool) Member { 163 var name, email string 164 cozyURL := c.PrimaryCozyURL() 165 addr, err := c.ToMailAddress() 166 if err == nil { 167 name = addr.Name 168 email = addr.Email 169 } else { 170 name = c.PrimaryName() 171 } 172 return Member{ 173 Status: MemberStatusMailNotSent, 174 Name: name, 175 Email: email, 176 Instance: cozyURL, 177 ReadOnly: readOnly, 178 } 179 } 180 181 // addMember adds a member to the members of the sharing if they are not yet in 182 // this list, and also adds credentials for them. It returns the state 183 // parameter of the credentials if added, and the index of the member. 184 func (s *Sharing) addMember(inst *instance.Instance, m Member) (string, int, error) { 185 idx := -1 186 for i, member := range s.Members { 187 if i == 0 { 188 continue // Skip the owner 189 } 190 var found bool 191 if m.Email != "" { 192 found = m.Email == member.Email 193 } else if m.Instance != "" { 194 found = m.Instance == member.Instance 195 } 196 if !found { 197 continue 198 } 199 if member.Status == MemberStatusReady { 200 return "", i, nil 201 } 202 idx = i 203 s.Members[i].Status = m.Status 204 s.Members[i].Name = m.Name 205 s.Members[i].Instance = m.Instance 206 s.Members[i].ReadOnly = m.ReadOnly 207 break 208 } 209 if idx < 1 { 210 if len(s.Members) >= maxNumberOfMembers(inst) { 211 return "", -1, ErrTooManyMembers 212 } 213 s.Members = append(s.Members, m) 214 } 215 state := crypto.Base64Encode(crypto.GenerateRandomBytes(StateLen)) 216 creds := Credentials{ 217 State: string(state), 218 XorKey: MakeXorKey(), 219 } 220 if idx < 1 { 221 s.Credentials = append(s.Credentials, creds) 222 idx = len(s.Credentials) 223 } else { 224 s.Credentials[idx-1] = creds 225 } 226 return creds.State, idx, nil 227 } 228 229 // APIDelegateAddContacts is used to serialize a request to add contacts to 230 // JSON-API 231 type APIDelegateAddContacts struct { 232 sid string 233 members []Member 234 groups []Group 235 } 236 237 // ID returns the sharing qualified identifier 238 func (a *APIDelegateAddContacts) ID() string { return a.sid } 239 240 // Rev returns the sharing revision 241 func (a *APIDelegateAddContacts) Rev() string { return "" } 242 243 // DocType returns the sharing document type 244 func (a *APIDelegateAddContacts) DocType() string { return consts.Sharings } 245 246 // SetID changes the sharing qualified identifier 247 func (a *APIDelegateAddContacts) SetID(id string) {} 248 249 // SetRev changes the sharing revision 250 func (a *APIDelegateAddContacts) SetRev(rev string) {} 251 252 // Clone is part of jsonapi.Object interface 253 func (a *APIDelegateAddContacts) Clone() couchdb.Doc { 254 panic("APIDelegateAddContacts must not be cloned") 255 } 256 257 // Links is part of jsonapi.Object interface 258 func (a *APIDelegateAddContacts) Links() *jsonapi.LinksList { return nil } 259 260 // Included is part of jsonapi.Object interface 261 func (a *APIDelegateAddContacts) Included() []jsonapi.Object { return nil } 262 263 // Relationships is part of jsonapi.Object interface 264 func (a *APIDelegateAddContacts) Relationships() jsonapi.RelationshipMap { 265 return jsonapi.RelationshipMap{ 266 "recipients": jsonapi.Relationship{ 267 Data: a.members, 268 }, 269 "groups": jsonapi.Relationship{ 270 Data: a.groups, 271 }, 272 } 273 } 274 275 var _ jsonapi.Object = (*APIDelegateAddContacts)(nil) 276 277 // DelegateAddContactsAndGroups adds a list of contacts and groups on a 278 // recipient cozy. Part of the work is delegated to owner cozy, but the 279 // invitation mail is still sent from the recipient cozy. 280 func (s *Sharing) DelegateAddContactsAndGroups(inst *instance.Instance, groupIDs, contactIDs []string, readOnly bool) error { 281 api := &APIDelegateAddContacts{} 282 api.sid = s.SID 283 284 for _, id := range contactIDs { 285 c, err := contact.Find(inst, id) 286 if err != nil { 287 return err 288 } 289 var name, email string 290 cozyURL := c.PrimaryCozyURL() 291 addr, err := c.ToMailAddress() 292 if err == nil { 293 name = addr.Name 294 email = addr.Email 295 } else { 296 if cozyURL == "" { 297 return err 298 } 299 name = c.PrimaryName() 300 } 301 m := Member{ 302 Status: MemberStatusMailNotSent, 303 Name: name, 304 Email: email, 305 Instance: cozyURL, 306 ReadOnly: readOnly, 307 } 308 api.members = append(api.members, m) 309 } 310 311 for _, groupID := range groupIDs { 312 group, err := contact.FindGroup(inst, groupID) 313 if err != nil { 314 return err 315 } 316 g := Group{ID: groupID, Name: group.Name(), ReadOnly: readOnly} 317 api.groups = append(api.groups, g) 318 319 contacts, err := group.GetAllContacts(inst) 320 if err != nil { 321 return err 322 } 323 groupIndex := len(s.Groups) 324 for _, contact := range contacts { 325 m := buildMemberFromContact(contact, readOnly) 326 m.Groups = []int{groupIndex} 327 m.OnlyInGroups = true 328 api.members = append(api.members, m) 329 } 330 } 331 332 return s.SendDelegated(inst, api) 333 } 334 335 // SendDelegated calls the delegated endpoint on the sharer to adds 336 // contacts/groups. 337 func (s *Sharing) SendDelegated(inst *instance.Instance, api *APIDelegateAddContacts) error { 338 data, err := jsonapi.MarshalObject(api) 339 if err != nil { 340 return err 341 } 342 body, err := json.Marshal(jsonapi.Document{Data: &data}) 343 if err != nil { 344 return err 345 } 346 u, err := url.Parse(s.Members[0].Instance) 347 if err != nil { 348 return err 349 } 350 c := &s.Credentials[0] 351 if c.AccessToken == nil { 352 return ErrInvalidSharing 353 } 354 opts := &request.Options{ 355 Method: http.MethodPost, 356 Scheme: u.Scheme, 357 Domain: u.Host, 358 Path: "/sharings/" + s.SID + "/recipients/delegated", 359 Headers: request.Headers{ 360 echo.HeaderAccept: echo.MIMEApplicationJSON, 361 echo.HeaderContentType: jsonapi.ContentType, 362 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 363 }, 364 Body: bytes.NewReader(body), 365 ParseError: ParseRequestError, 366 } 367 res, err := request.Req(opts) 368 if res != nil && res.StatusCode/100 == 4 { 369 res, err = RefreshToken(inst, err, s, &s.Members[0], c, opts, body) 370 } 371 if err != nil { 372 return err 373 } 374 defer res.Body.Close() 375 if res.StatusCode != http.StatusOK { 376 return ErrInternalServerError 377 } 378 var states map[string]string 379 if err = json.NewDecoder(res.Body).Decode(&states); err != nil { 380 return err 381 } 382 383 // We can have conflicts when updating the sharing document, so we are 384 // retrying when it is the case. 385 maxRetries := 3 386 i := 0 387 for { 388 for _, m := range api.members { 389 found := false 390 for i, member := range s.Members { 391 if i == 0 { 392 continue // skip the owner 393 } 394 if m.Email == "" { 395 found = m.Instance == member.Instance 396 } else { 397 found = m.Email == member.Email 398 } 399 if found && member.Status != MemberStatusReady { 400 s.Members[i].Status = m.Status 401 s.Members[i].Name = m.Name 402 s.Members[i].Instance = m.Instance 403 s.Members[i].ReadOnly = m.ReadOnly 404 break 405 } 406 } 407 if !found { 408 s.Members = append(s.Members, m) 409 } 410 } 411 if err := couchdb.UpdateDoc(inst, s); err == nil { 412 break 413 } 414 i++ 415 if i > maxRetries { 416 return err 417 } 418 time.Sleep(1 * time.Second) 419 s, err = FindSharing(inst, s.SID) 420 if err != nil { 421 return err 422 } 423 } 424 return s.SendInvitationsToMembers(inst, api.members, states) 425 } 426 427 // AddDelegatedContact adds a contact on the owner cozy, but for a contact from 428 // a recipient (open_sharing: true only) 429 func (s *Sharing) AddDelegatedContact(inst *instance.Instance, m Member) (string, error) { 430 m.Status = MemberStatusPendingInvitation 431 if m.Instance != "" || m.Email == "" { 432 m.Status = MemberStatusMailNotSent 433 } 434 state, _, err := s.addMember(inst, m) 435 if err != nil { 436 return "", err 437 } 438 if state == "" { 439 return "", ErrAlreadyAccepted 440 } 441 return state, nil 442 } 443 444 // DelegateDiscovery delegates the POST discovery when a recipient has invited 445 // another person to a sharing, and this person accepts the sharing on the 446 // recipient cozy. The calls is delegated to the owner cozy. 447 func (s *Sharing) DelegateDiscovery(inst *instance.Instance, state, cozyURL string) (string, error) { 448 u, err := url.Parse(s.Members[0].Instance) 449 if err != nil { 450 return "", err 451 } 452 v := url.Values{} 453 v.Add("state", state) 454 v.Add("url", cozyURL) 455 body := []byte(v.Encode()) 456 c := &s.Credentials[0] 457 opts := &request.Options{ 458 Method: http.MethodPost, 459 Scheme: u.Scheme, 460 Domain: u.Host, 461 Path: "/sharings/" + s.SID + "/discovery", 462 Headers: request.Headers{ 463 echo.HeaderAccept: echo.MIMEApplicationJSON, 464 echo.HeaderContentType: echo.MIMEApplicationForm, 465 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 466 }, 467 Body: bytes.NewReader(body), 468 ParseError: ParseRequestError, 469 } 470 res, err := request.Req(opts) 471 if res != nil && res.StatusCode/100 == 4 { 472 res, err = RefreshToken(inst, err, s, &s.Members[0], c, opts, body) 473 } 474 if err != nil { 475 if res != nil && res.StatusCode == http.StatusBadRequest { 476 return "", ErrInvalidURL 477 } 478 return "", err 479 } 480 defer res.Body.Close() 481 var success map[string]string 482 if err = json.NewDecoder(res.Body).Decode(&success); err != nil { 483 return "", err 484 } 485 PersistInstanceURL(inst, success["email"], cozyURL) 486 return success["redirect"], nil 487 } 488 489 // UpdateRecipients updates the lists of members and groups. 490 func (s *Sharing) UpdateRecipients(inst *instance.Instance, params PutRecipientsParams) error { 491 for i, m := range params.Members { 492 if i >= len(s.Members) { 493 s.Members = append(s.Members, Member{}) 494 } 495 if m.Email != s.Members[i].Email && m.Email != "" { 496 if c, err := contact.FindByEmail(inst, m.Email); err == nil { 497 s.Members[i].Name = c.PrimaryName() 498 } 499 } 500 s.Members[i].Email = m.Email 501 s.Members[i].PublicName = m.PublicName 502 s.Members[i].Status = m.Status 503 s.Members[i].ReadOnly = m.ReadOnly 504 } 505 s.Groups = params.Groups 506 return couchdb.UpdateDoc(inst, s) 507 } 508 509 // PersistInstanceURL updates the io.cozy.contacts document with the Cozy 510 // instance URL, and fills the fullname if it was missing. 511 func PersistInstanceURL(inst *instance.Instance, email, cozyURL string) { 512 if email == "" || cozyURL == "" { 513 return 514 } 515 c, err := contact.FindByEmail(inst, email) 516 if err != nil { 517 return 518 } 519 if err := c.AddCozyURL(inst, cozyURL); err != nil { 520 inst.Logger().WithNamespace("sharing"). 521 Warnf("Error on saving contact: %s", err) 522 } 523 } 524 525 // FindMemberByState returns the member that is linked to the sharing by 526 // the given state 527 func (s *Sharing) FindMemberByState(state string) (*Member, error) { 528 if !s.Owner { 529 return nil, ErrInvalidSharing 530 } 531 for i, c := range s.Credentials { 532 if c.State == state { 533 if len(s.Members) <= i+1 { 534 return nil, ErrInvalidSharing 535 } 536 return &s.Members[i+1], nil 537 } 538 } 539 return nil, ErrMemberNotFound 540 } 541 542 // FindMemberBySharecode returns the member that is linked to the sharing by 543 // the given sharecode 544 func (s *Sharing) FindMemberBySharecode(db prefixer.Prefixer, sharecode string) (*Member, error) { 545 perms, err := permission.GetForSharePreview(db, s.SID) 546 if err != nil { 547 return nil, err 548 } 549 return s.FindMemberByCode(perms, sharecode) 550 } 551 552 // FindMemberByInteractCode returns the member that is linked to the sharing by 553 // the given sharecode via a share-interact permission. 554 func (s *Sharing) FindMemberByInteractCode(db prefixer.Prefixer, sharecode string) (*Member, error) { 555 perms, err := permission.GetForShareInteract(db, s.SID) 556 if err != nil { 557 return nil, err 558 } 559 return s.FindMemberByCode(perms, sharecode) 560 } 561 562 // FindMemberByCode returns the member that is linked to the sharing by the 563 // given code. 564 func (s *Sharing) FindMemberByCode(perms *permission.Permission, sharecode string) (*Member, error) { 565 var emailOrInstance string 566 for e, code := range perms.Codes { 567 if code == sharecode { 568 emailOrInstance = e 569 break 570 } 571 } 572 if emailOrInstance == "" { 573 for e, code := range perms.ShortCodes { 574 if code == sharecode { 575 emailOrInstance = e 576 break 577 } 578 } 579 } 580 if emailOrInstance == "" { 581 return nil, ErrMemberNotFound 582 } 583 584 for i, m := range s.Members { 585 if m.Email == emailOrInstance { 586 return &s.Members[i], nil 587 } 588 } 589 for i, m := range s.Members { 590 if m.Instance == emailOrInstance { 591 return &s.Members[i], nil 592 } 593 } 594 if strings.HasPrefix(emailOrInstance, "index:") { 595 i, err := strconv.Atoi(strings.TrimPrefix(emailOrInstance, "index:")) 596 if err == nil && i > 0 && i < len(s.Members) { 597 return &s.Members[i], nil 598 } 599 } 600 return nil, ErrMemberNotFound 601 } 602 603 // FindMemberByInboundClientID returns the member that have used this client 604 // ID to make a request on the given sharing 605 func (s *Sharing) FindMemberByInboundClientID(clientID string) (*Member, error) { 606 if s.Owner { 607 for i, c := range s.Credentials { 608 if c.InboundClientID == clientID { 609 return &s.Members[i+1], nil 610 } 611 } 612 return nil, ErrMemberNotFound 613 } 614 615 if s.Credentials[0].InboundClientID == clientID { 616 return &s.Members[0], nil 617 } 618 return nil, ErrMemberNotFound 619 } 620 621 // FindCredentials returns the credentials for the given member 622 func (s *Sharing) FindCredentials(m *Member) *Credentials { 623 if s.Owner { 624 for i, member := range s.Members { 625 if i > 0 && m.Same(member) { 626 return &s.Credentials[i-1] 627 } 628 } 629 return nil 630 } 631 632 if m.Same(s.Members[0]) { 633 return &s.Credentials[0] 634 } 635 return nil 636 } 637 638 // Refresh will refresh the access token, and persist the new access token in 639 // the sharing 640 func (c *Credentials) Refresh(inst *instance.Instance, s *Sharing, m *Member) error { 641 if c.Client == nil || c.AccessToken == nil { 642 return ErrNoOAuthClient 643 } 644 u, err := url.Parse(m.Instance) 645 if err != nil { 646 return err 647 } 648 r := &auth.Request{ 649 Scheme: u.Scheme, 650 Domain: u.Host, 651 } 652 token, err := r.RefreshToken(c.Client, c.AccessToken) 653 if err != nil { 654 return err 655 } 656 c.AccessToken.AccessToken = token.AccessToken 657 if err = couchdb.UpdateDoc(inst, s); err != nil && !couchdb.IsConflictError(err) { 658 return err 659 } 660 return nil 661 } 662 663 // AddReadOnlyFlag adds the read-only flag of a recipient, and send 664 // an access token with a short validity to let it synchronize its last 665 // changes. 666 func (s *Sharing) AddReadOnlyFlag(inst *instance.Instance, index int) error { 667 if index <= 1 { 668 return ErrMemberNotFound 669 } 670 if s.ReadOnlyFlag() { 671 return ErrInvalidSharing 672 } 673 if s.Members[index].ReadOnly { 674 return nil 675 } 676 s.Members[index].ReadOnly = true 677 678 ac := APICredentials{ 679 CID: s.SID, 680 Credentials: &Credentials{}, 681 } 682 // We can't just revoke the tokens for the recipient (they are persisted 683 // on this cozy), so we have to revoke the client. And we create a new 684 // client for the temporary access token used to synchronize the last 685 // changes (the recipient won't have a refresh token). 686 cli, err := CreateOAuthClient(inst, &s.Members[index]) 687 if err != nil { 688 return err 689 } 690 s.Credentials[index-1].InboundClientID = cli.ClientID 691 ac.Credentials.Client = ConvertOAuthClient(cli) 692 scope := consts.Sharings + ":ALL:" + s.SID 693 issuedAt := time.Now().Add(1*time.Hour - consts.AccessTokenValidityDuration) 694 access, err := inst.MakeJWT(consts.AccessTokenAudience, cli.ClientID, scope, "", issuedAt) 695 if err != nil { 696 return err 697 } 698 ac.Credentials.AccessToken = &auth.AccessToken{ 699 TokenType: "bearer", 700 AccessToken: access, 701 // No refresh token 702 Scope: scope, 703 } 704 705 u, err := url.Parse(s.Members[index].Instance) 706 if s.Members[index].Instance == "" || err != nil { 707 return ErrInvalidSharing 708 } 709 data, err := jsonapi.MarshalObject(&ac) 710 if err != nil { 711 return err 712 } 713 body, err := json.Marshal(jsonapi.Document{Data: &data}) 714 if err != nil { 715 return err 716 } 717 opts := &request.Options{ 718 Method: http.MethodPost, 719 Scheme: u.Scheme, 720 Domain: u.Host, 721 Path: "/sharings/" + s.SID + "/recipients/self/readonly", 722 Headers: request.Headers{ 723 echo.HeaderAccept: jsonapi.ContentType, 724 echo.HeaderContentType: jsonapi.ContentType, 725 echo.HeaderAuthorization: "Bearer " + s.Credentials[index-1].AccessToken.AccessToken, 726 }, 727 Body: bytes.NewReader(body), 728 ParseError: ParseRequestError, 729 } 730 res, err := request.Req(opts) 731 if res != nil && res.StatusCode/100 == 4 { 732 res, err = RefreshToken(inst, err, s, &s.Members[index], &s.Credentials[index-1], opts, body) 733 } 734 if err != nil { 735 if res != nil { 736 return ErrRequestFailed 737 } 738 return err 739 } 740 defer res.Body.Close() 741 742 return couchdb.UpdateDoc(inst, s) 743 } 744 745 // DelegateAddReadOnlyFlag is used by a recipient to ask the sharer to 746 // add the read-only falg for another member of the sharing. 747 func (s *Sharing) DelegateAddReadOnlyFlag(inst *instance.Instance, index int) error { 748 u, err := url.Parse(s.Members[0].Instance) 749 if err != nil { 750 return err 751 } 752 c := &s.Credentials[0] 753 opts := &request.Options{ 754 Method: http.MethodPost, 755 Scheme: u.Scheme, 756 Domain: u.Host, 757 Path: fmt.Sprintf("/sharings/%s/recipients/%d/readonly", s.SID, index), 758 Headers: request.Headers{ 759 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 760 }, 761 ParseError: ParseRequestError, 762 } 763 res, err := request.Req(opts) 764 if res != nil && res.StatusCode/100 == 4 { 765 res, err = RefreshToken(inst, err, s, &s.Members[0], c, opts, nil) 766 } 767 if err != nil { 768 if res != nil && res.StatusCode == http.StatusBadRequest { 769 return ErrInvalidURL 770 } 771 return err 772 } 773 res.Body.Close() 774 return nil 775 } 776 777 // DowngradeToReadOnly is used to receive credentials on a read-write instance 778 // to sync the last changes before going to read-only mode. 779 func (s *Sharing) DowngradeToReadOnly(inst *instance.Instance, creds *APICredentials) error { 780 if s.Owner { 781 return ErrInvalidSharing 782 } 783 784 for i, m := range s.Members { 785 if i > 0 && m.Instance != "" { 786 s.Members[i].ReadOnly = true 787 break 788 } 789 } 790 791 s.Credentials[0].AccessToken = creds.AccessToken 792 s.Credentials[0].Client = creds.Client 793 794 if err := removeSharingTrigger(inst, s.Triggers.ReplicateID); err != nil { 795 return err 796 } 797 s.Triggers.ReplicateID = "" 798 if err := removeSharingTrigger(inst, s.Triggers.UploadID); err != nil { 799 return err 800 } 801 s.Triggers.UploadID = "" 802 803 if err := couchdb.UpdateDoc(inst, s); err != nil { 804 return err 805 } 806 807 s.pushJob(inst, "share-replicate") 808 if s.FirstFilesRule() != nil { 809 s.pushJob(inst, "share-upload") 810 } 811 return nil 812 } 813 814 // RemoveReadOnlyFlag removes the read-only flag of a recipient, and send 815 // credentials to their cozy so that it can push its changes. 816 func (s *Sharing) RemoveReadOnlyFlag(inst *instance.Instance, index int) error { 817 if index <= 1 { 818 return ErrMemberNotFound 819 } 820 if s.ReadOnlyFlag() { 821 return ErrInvalidSharing 822 } 823 if !s.Members[index].ReadOnly { 824 return nil 825 } 826 s.Members[index].ReadOnly = false 827 828 ac := APICredentials{ 829 CID: s.SID, 830 Credentials: &Credentials{ 831 XorKey: s.Credentials[index-1].XorKey, 832 }, 833 } 834 // Create the credentials for the recipient 835 cli, err := CreateOAuthClient(inst, &s.Members[index]) 836 if err != nil { 837 return err 838 } 839 s.Credentials[index-1].InboundClientID = cli.ClientID 840 ac.Credentials.Client = ConvertOAuthClient(cli) 841 token, err := CreateAccessToken(inst, cli, s.SID, permission.ALL) 842 if err != nil { 843 return err 844 } 845 ac.Credentials.AccessToken = token 846 847 u, err := url.Parse(s.Members[index].Instance) 848 if s.Members[index].Instance == "" || err != nil { 849 return ErrInvalidSharing 850 } 851 data, err := jsonapi.MarshalObject(&ac) 852 if err != nil { 853 return err 854 } 855 body, err := json.Marshal(jsonapi.Document{Data: &data}) 856 if err != nil { 857 return err 858 } 859 opts := &request.Options{ 860 Method: http.MethodDelete, 861 Scheme: u.Scheme, 862 Domain: u.Host, 863 Path: "/sharings/" + s.SID + "/recipients/self/readonly", 864 Headers: request.Headers{ 865 echo.HeaderAccept: jsonapi.ContentType, 866 echo.HeaderContentType: jsonapi.ContentType, 867 echo.HeaderAuthorization: "Bearer " + s.Credentials[index-1].AccessToken.AccessToken, 868 }, 869 Body: bytes.NewReader(body), 870 ParseError: ParseRequestError, 871 } 872 res, err := request.Req(opts) 873 if res != nil && res.StatusCode/100 == 4 { 874 res, err = RefreshToken(inst, err, s, &s.Members[index], &s.Credentials[index-1], opts, body) 875 } 876 if err != nil { 877 return err 878 } 879 res.Body.Close() 880 return couchdb.UpdateDoc(inst, s) 881 } 882 883 // UpgradeToReadWrite is used to receive credentials on a read-only instance 884 // to upgrade it to read-write. 885 func (s *Sharing) UpgradeToReadWrite(inst *instance.Instance, creds *APICredentials) error { 886 if s.Owner { 887 return ErrInvalidSharing 888 } 889 890 for i, m := range s.Members { 891 if i > 0 && m.Instance != "" { 892 s.Members[i].ReadOnly = false 893 break 894 } 895 } 896 897 if err := s.SetupReceiver(inst); err != nil { 898 return err 899 } 900 901 s.Credentials[0].XorKey = creds.XorKey 902 s.Credentials[0].AccessToken = creds.AccessToken 903 s.Credentials[0].Client = creds.Client 904 return couchdb.UpdateDoc(inst, s) 905 } 906 907 // DelegateRemoveReadOnlyFlag is used by a recipient to ask the sharer to 908 // remove the read-only falg for another member of the sharing. 909 func (s *Sharing) DelegateRemoveReadOnlyFlag(inst *instance.Instance, index int) error { 910 u, err := url.Parse(s.Members[0].Instance) 911 if err != nil { 912 return err 913 } 914 c := &s.Credentials[0] 915 opts := &request.Options{ 916 Method: http.MethodDelete, 917 Scheme: u.Scheme, 918 Domain: u.Host, 919 Path: fmt.Sprintf("/sharings/%s/recipients/%d/readonly", s.SID, index), 920 Headers: request.Headers{ 921 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 922 }, 923 ParseError: ParseRequestError, 924 } 925 res, err := request.Req(opts) 926 if res != nil && res.StatusCode/100 == 4 { 927 res, err = RefreshToken(inst, err, s, &s.Members[0], c, opts, nil) 928 } 929 if err != nil { 930 if res != nil && res.StatusCode == http.StatusBadRequest { 931 return ErrInvalidURL 932 } 933 return err 934 } 935 res.Body.Close() 936 return nil 937 } 938 939 // RevokeMember revoke the access granted to a member and contact it 940 func (s *Sharing) RevokeMember(inst *instance.Instance, index int) error { 941 m := &s.Members[index] 942 c := &s.Credentials[index-1] 943 944 // No need to contact the revoked member if the sharing is not ready 945 if m.Status == MemberStatusReady { 946 if err := s.NotifyMemberRevocation(inst, m, c); err != nil { 947 inst.Logger().WithNamespace("sharing"). 948 Warnf("Error on revocation notification: %s", err) 949 } 950 951 if err := DeleteOAuthClient(inst, m, c); err != nil { 952 return err 953 } 954 } 955 956 // We may have concurrency issues where RevokeMember is called several 957 // times on different goroutines/processes. So, we may need to retry the 958 // operation several times. 959 leftRetries := 3 960 for { 961 m.Status = MemberStatusRevoked 962 // Do not remove the credentials from the array to preserve the members / 963 // credentials order, just empty them 964 *c = Credentials{} 965 966 err := couchdb.UpdateDoc(inst, s) 967 if !couchdb.IsConflictError(err) || leftRetries == 0 { 968 return err 969 } 970 971 if errg := couchdb.GetDoc(inst, consts.Sharings, s.SID, s); errg != nil { 972 return err 973 } 974 m = &s.Members[index] 975 c = &s.Credentials[index-1] 976 leftRetries-- 977 } 978 } 979 980 // RevokeOwner revoke the access granted to the owner and notify it 981 func (s *Sharing) RevokeOwner(inst *instance.Instance) error { 982 if s.Credentials == nil { // Already revoked 983 return nil 984 } 985 986 m := &s.Members[0] 987 c := &s.Credentials[0] 988 989 if err := s.NotifyMemberRevocation(inst, m, c); err != nil { 990 inst.Logger().WithNamespace("sharing"). 991 Warnf("Error on revocation notification: %s", err) 992 } 993 if err := DeleteOAuthClient(inst, m, c); err != nil { 994 return err 995 } 996 m.Status = MemberStatusRevoked 997 s.Credentials = nil 998 s.Active = false 999 return couchdb.UpdateDoc(inst, s) 1000 } 1001 1002 // NotifyMemberRevocation send a notification to this member that he/she was 1003 // revoked from this sharing 1004 func (s *Sharing) NotifyMemberRevocation(inst *instance.Instance, m *Member, c *Credentials) error { 1005 u, err := url.Parse(m.Instance) 1006 if m.Instance == "" || err != nil { 1007 return ErrInvalidSharing 1008 } 1009 if c.Client == nil || c.AccessToken == nil { 1010 return ErrNoOAuthClient 1011 } 1012 1013 var path string 1014 if m.Status == MemberStatusOwner { 1015 path = "/sharings/" + s.SID + "/answer" 1016 } else { 1017 path = "/sharings/" + s.SID 1018 } 1019 1020 opts := &request.Options{ 1021 Method: http.MethodDelete, 1022 Scheme: u.Scheme, 1023 Domain: u.Host, 1024 Path: path, 1025 Headers: request.Headers{ 1026 echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken, 1027 }, 1028 ParseError: ParseRequestError, 1029 } 1030 res, err := request.Req(opts) 1031 if res != nil && res.StatusCode/100 == 4 { 1032 res, err = RefreshToken(inst, err, s, m, c, opts, nil) 1033 } 1034 if err != nil { 1035 if res != nil && res.StatusCode/100 == 5 { 1036 return ErrInternalServerError 1037 } 1038 return err 1039 } 1040 res.Body.Close() 1041 return nil 1042 } 1043 1044 // PutRecipientsParams is the body of the request for updating the list of 1045 // members and groups on the active recipients of a sharing. 1046 type PutRecipientsParams struct { 1047 Members []Member `json:"data"` 1048 Groups []Group `json:"included"` 1049 } 1050 1051 // NotifyRecipients will push the updated list of members of the sharing to the 1052 // active recipients. It is meant to be used in a goroutine, errors are just 1053 // logged (nothing critical here). 1054 func (s *Sharing) NotifyRecipients(inst *instance.Instance, except *Member) { 1055 if !s.Owner { 1056 return 1057 } 1058 if len(s.Members) != len(s.Credentials)+1 { 1059 return 1060 } 1061 1062 defer func() { 1063 if r := recover(); r != nil { 1064 var err error 1065 switch r := r.(type) { 1066 case error: 1067 err = r 1068 default: 1069 err = fmt.Errorf("%v", r) 1070 } 1071 stack := make([]byte, 4<<10) // 4 KB 1072 length := runtime.Stack(stack, false) 1073 log := inst.Logger().WithNamespace("sharing").WithField("panic", true) 1074 log.Errorf("PANIC RECOVER %s: %s", err.Error(), stack[:length]) 1075 } 1076 }() 1077 1078 // XXX Wait a bit to avoid pressure on recipients cozy after delegated operations 1079 time.Sleep(3 * time.Second) 1080 1081 active := false 1082 for i := range s.Members { 1083 if shouldNotifyMember(s, i, except) { 1084 active = true 1085 break 1086 } 1087 } 1088 if !active { 1089 return 1090 } 1091 1092 var params PutRecipientsParams 1093 params.Members = make([]Member, len(s.Members)) 1094 for i, m := range s.Members { 1095 params.Members[i] = Member{ 1096 Status: m.Status, 1097 PublicName: m.PublicName, 1098 Email: m.Email, 1099 ReadOnly: m.ReadOnly, 1100 // Instance and name are private 1101 } 1102 } 1103 params.Groups = make([]Group, len(s.Groups)) 1104 copy(params.Groups, s.Groups) 1105 body, err := json.Marshal(params) 1106 if err != nil { 1107 inst.Logger().WithNamespace("sharing"). 1108 Warnf("Can't serialize the updated members list for %s: %s", s.SID, err) 1109 return 1110 } 1111 1112 for i, m := range s.Members { 1113 if !shouldNotifyMember(s, i, except) { 1114 continue 1115 } 1116 u, err := url.Parse(m.Instance) 1117 if m.Instance == "" || err != nil { 1118 inst.Logger().WithNamespace("sharing"). 1119 Infof("Invalid instance URL %s: %s", m.Instance, err) 1120 continue 1121 } 1122 c := &s.Credentials[i-1] 1123 var token string 1124 if m.Status == MemberStatusReady { 1125 token = c.AccessToken.AccessToken 1126 } else { 1127 perms, err := permission.GetForSharePreview(inst, s.SID) 1128 if err == nil { 1129 token = perms.Codes[m.Email] 1130 } 1131 if token == "" { 1132 continue 1133 } 1134 } 1135 opts := &request.Options{ 1136 Method: http.MethodPut, 1137 Scheme: u.Scheme, 1138 Domain: u.Host, 1139 Path: "/sharings/" + s.SID + "/recipients", 1140 Headers: request.Headers{ 1141 echo.HeaderAccept: jsonapi.ContentType, 1142 echo.HeaderContentType: jsonapi.ContentType, 1143 echo.HeaderAuthorization: "Bearer " + token, 1144 }, 1145 Body: bytes.NewReader(body), 1146 ParseError: ParseRequestError, 1147 } 1148 res, err := request.Req(opts) 1149 if res != nil && res.StatusCode/100 == 4 { 1150 res, err = RefreshToken(inst, err, s, &s.Members[i], c, opts, body) 1151 } 1152 if err != nil { 1153 inst.Logger().WithNamespace("sharing"). 1154 Debugf("Can't notify %#v about the updated members list: %s", m, err) 1155 continue 1156 } 1157 res.Body.Close() 1158 } 1159 } 1160 1161 func shouldNotifyMember(s *Sharing, i int, except *Member) bool { 1162 if i == 0 { // Don't notify the owner 1163 return false 1164 } 1165 if &s.Members[i] == except { 1166 return false 1167 } 1168 m := s.Members[i] 1169 if m.Status == MemberStatusReady { 1170 return true 1171 } 1172 if m.Status != MemberStatusPendingInvitation && m.Status != MemberStatusSeen { 1173 return false 1174 } 1175 return m.Instance != "" 1176 } 1177 1178 // SaveBitwarden adds the sharing member to the bitwarden organization in the 1179 // sharing rules. 1180 func (s *Sharing) SaveBitwarden(inst *instance.Instance, m *Member, bw *APIBitwarden) error { 1181 rule := s.FirstBitwardenOrganizationRule() 1182 if rule == nil || len(rule.Values) == 0 { 1183 return nil 1184 } 1185 1186 org := &bitwarden.Organization{} 1187 if err := couchdb.GetDoc(inst, consts.BitwardenOrganizations, rule.Values[0], org); err != nil { 1188 return err 1189 } 1190 domain := m.Instance 1191 if u, err := url.Parse(m.Instance); err == nil { 1192 domain = u.Host 1193 } 1194 orgKey := org.Members[domain].OrgKey 1195 1196 if bw.PublicKey != "" { 1197 contact := &bitwarden.Contact{} 1198 err := couchdb.GetDoc(inst, consts.BitwardenContacts, bw.UserID, contact) 1199 if couchdb.IsNotFoundError(err) { 1200 md := metadata.New() 1201 md.DocTypeVersion = bitwarden.DocTypeVersion 1202 contact.UserID = bw.UserID 1203 contact.Email = m.Email 1204 contact.PublicKey = bw.PublicKey 1205 contact.Metadata = *md 1206 err = couchdb.CreateNamedDocWithDB(inst, contact) 1207 } 1208 if err != nil { 1209 return err 1210 } 1211 // The public key can have been changed if the member has reset their password 1212 if contact.PublicKey != bw.PublicKey { 1213 contact.UserID = bw.UserID 1214 contact.PublicKey = bw.PublicKey 1215 contact.Confirmed = false 1216 contact.Metadata.UpdatedAt = time.Now() 1217 if err := couchdb.UpdateDoc(inst, contact); err != nil { 1218 return err 1219 } 1220 // The organization key was encrypted with the wrong key and must be discarded 1221 orgKey = "" 1222 } 1223 } 1224 1225 status := bitwarden.OrgMemberInvited 1226 if bw.PublicKey != "" { 1227 status = bitwarden.OrgMemberAccepted 1228 } 1229 if orgKey != "" { 1230 status = bitwarden.OrgMemberConfirmed 1231 } 1232 org.Members[domain] = bitwarden.OrgMember{ 1233 UserID: bw.UserID, 1234 Email: m.Email, 1235 Name: m.PrimaryName(), 1236 OrgKey: orgKey, 1237 Status: status, 1238 Owner: false, 1239 ReadOnly: m.ReadOnly || s.ReadOnlyRules(), 1240 } 1241 if err := couchdb.UpdateDoc(inst, org); err != nil { 1242 return err 1243 } 1244 if status == bitwarden.OrgMemberAccepted { 1245 return s.sendContactConfirmationMail(inst, m) 1246 } 1247 return nil 1248 } 1249 1250 func (s *Sharing) sendContactConfirmationMail(inst *instance.Instance, m *Member) error { 1251 publicName, _ := csettings.PublicName(inst) 1252 link := inst.SubDomain(s.AppSlug) 1253 msg, err := job.NewMessage(&mail.Options{ 1254 Mode: mail.ModeFromStack, 1255 TemplateName: "sharing_to_confirm", 1256 TemplateValues: map[string]interface{}{ 1257 "PublicName": publicName, 1258 "MemberName": m.PrimaryName(), 1259 "Link": link.String(), 1260 }, 1261 }) 1262 if err != nil { 1263 return err 1264 } 1265 _, err = job.System().PushJob(inst, &job.JobRequest{ 1266 WorkerType: "sendmail", 1267 Message: msg, 1268 }) 1269 return err 1270 } 1271 1272 // RemoveBitwardenMember removes a sharing member from the bitwarden 1273 // organization. It is called when the owner revokes a member, or when the 1274 // owner is notified that a member has left the sharing. 1275 func (s *Sharing) RemoveBitwardenMember(inst *instance.Instance, m *Member, orgID string) error { 1276 org := &bitwarden.Organization{} 1277 if err := couchdb.GetDoc(inst, consts.BitwardenOrganizations, orgID, org); err != nil { 1278 if couchdb.IsNotFoundError(err) { 1279 return nil 1280 } 1281 return err 1282 } 1283 domain := m.Instance 1284 if u, err := url.Parse(m.Instance); err == nil { 1285 domain = u.Host 1286 } 1287 delete(org.Members, domain) 1288 return couchdb.UpdateDoc(inst, org) 1289 } 1290 1291 // RemoveAllBitwardenMembers removes all the members from the bitwarden 1292 // organization (except the owner of the sharing). 1293 func (s *Sharing) RemoveAllBitwardenMembers(inst *instance.Instance, orgID string) error { 1294 org := &bitwarden.Organization{} 1295 if err := couchdb.GetDoc(inst, consts.BitwardenOrganizations, orgID, org); err != nil { 1296 if couchdb.IsNotFoundError(err) { 1297 return nil 1298 } 1299 return err 1300 } 1301 for domain := range org.Members { 1302 if domain != inst.Domain { 1303 delete(org.Members, domain) 1304 } 1305 } 1306 return couchdb.UpdateDoc(inst, org) 1307 }