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