github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/sharing.go (about) 1 package sharing 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "strings" 12 "time" 13 14 "github.com/cozy/cozy-stack/client/request" 15 "github.com/cozy/cozy-stack/model/app" 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/model/vfs" 23 "github.com/cozy/cozy-stack/pkg/consts" 24 "github.com/cozy/cozy-stack/pkg/couchdb" 25 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 26 "github.com/cozy/cozy-stack/pkg/couchdb/revision" 27 "github.com/cozy/cozy-stack/pkg/crypto" 28 "github.com/cozy/cozy-stack/pkg/jsonapi" 29 "github.com/cozy/cozy-stack/pkg/metadata" 30 "github.com/cozy/cozy-stack/pkg/prefixer" 31 "github.com/cozy/cozy-stack/pkg/realtime" 32 multierror "github.com/hashicorp/go-multierror" 33 "github.com/labstack/echo/v4" 34 ) 35 36 const ( 37 // StateLen is the number of bytes for the OAuth state parameter 38 StateLen = 16 39 ) 40 41 // Triggers keep record of which triggers are active 42 type Triggers struct { 43 TrackID string `json:"track_id,omitempty"` // Legacy 44 TrackIDs []string `json:"track_ids,omitempty"` 45 ReplicateID string `json:"replicate_id,omitempty"` 46 UploadID string `json:"upload_id,omitempty"` 47 } 48 49 // Sharing contains all the information about a sharing. 50 type Sharing struct { 51 SID string `json:"_id,omitempty"` 52 SRev string `json:"_rev,omitempty"` 53 54 Triggers Triggers `json:"triggers"` 55 Active bool `json:"active,omitempty"` 56 Owner bool `json:"owner,omitempty"` 57 Open bool `json:"open_sharing,omitempty"` 58 Description string `json:"description,omitempty"` 59 AppSlug string `json:"app_slug"` 60 PreviewPath string `json:"preview_path,omitempty"` 61 CreatedAt time.Time `json:"created_at"` 62 UpdatedAt time.Time `json:"updated_at"` 63 NbFiles int `json:"initial_number_of_files_to_sync,omitempty"` 64 Initial bool `json:"initial_sync,omitempty"` 65 ShortcutID string `json:"shortcut_id,omitempty"` 66 MovedFrom string `json:"moved_from,omitempty"` 67 68 Rules []Rule `json:"rules"` 69 70 // Members[0] is the owner, Members[1...] are the recipients 71 Members []Member `json:"members"` 72 Groups []Group `json:"groups,omitempty"` 73 74 // On the owner, credentials[i] is associated to members[i+1] 75 // On a recipient, there is only credentials[0] (for the owner) 76 Credentials []Credentials `json:"credentials,omitempty"` 77 } 78 79 // ID returns the sharing qualified identifier 80 func (s *Sharing) ID() string { return s.SID } 81 82 // Rev returns the sharing revision 83 func (s *Sharing) Rev() string { return s.SRev } 84 85 // DocType returns the sharing document type 86 func (s *Sharing) DocType() string { return consts.Sharings } 87 88 // SetID changes the sharing qualified identifier 89 func (s *Sharing) SetID(id string) { s.SID = id } 90 91 // SetRev changes the sharing revision 92 func (s *Sharing) SetRev(rev string) { s.SRev = rev } 93 94 // Clone implements couchdb.Doc 95 func (s *Sharing) Clone() couchdb.Doc { 96 cloned := *s 97 cloned.Rules = make([]Rule, len(s.Rules)) 98 copy(cloned.Rules, s.Rules) 99 for i := range cloned.Rules { 100 cloned.Rules[i].Values = make([]string, len(s.Rules[i].Values)) 101 copy(cloned.Rules[i].Values, s.Rules[i].Values) 102 } 103 cloned.Members = make([]Member, len(s.Members)) 104 copy(cloned.Members, s.Members) 105 cloned.Credentials = make([]Credentials, len(s.Credentials)) 106 copy(cloned.Credentials, s.Credentials) 107 for i := range s.Credentials { 108 if s.Credentials[i].Client != nil { 109 cloned.Credentials[i].Client = s.Credentials[i].Client.Clone() 110 } 111 if s.Credentials[i].AccessToken != nil { 112 cloned.Credentials[i].AccessToken = s.Credentials[i].AccessToken.Clone() 113 } 114 cloned.Credentials[i].XorKey = make([]byte, len(s.Credentials[i].XorKey)) 115 copy(cloned.Credentials[i].XorKey, s.Credentials[i].XorKey) 116 } 117 return &cloned 118 } 119 120 // ReadOnlyFlag returns true only if the given instance is declared a read-only 121 // member of the sharing. 122 func (s *Sharing) ReadOnlyFlag() bool { 123 if !s.Owner { 124 for i, m := range s.Members { 125 if i == 0 { 126 continue // skip owner 127 } 128 if m.Instance != "" { 129 return m.ReadOnly 130 } 131 } 132 } 133 return false 134 } 135 136 // ReadOnlyRules returns true if the rules forbid that a change on the 137 // recipient's cozy instance can be propagated to the sharer's cozy. 138 func (s *Sharing) ReadOnlyRules() bool { 139 for _, rule := range s.Rules { 140 if rule.HasSync() { 141 return false 142 } 143 } 144 return true 145 } 146 147 // ReadOnly returns true if the member has the read-only flag, or if the rules 148 // forces a read-only mode. 149 func (s *Sharing) ReadOnly() bool { 150 return s.ReadOnlyFlag() || s.ReadOnlyRules() 151 } 152 153 // BeOwner initializes a sharing on the cozy of its owner 154 func (s *Sharing) BeOwner(inst *instance.Instance, slug string) error { 155 s.Active = true 156 s.Owner = true 157 if s.AppSlug == "" { 158 s.AppSlug = slug 159 } 160 if s.AppSlug == "" { 161 s.PreviewPath = "" 162 } 163 s.CreatedAt = time.Now() 164 s.UpdatedAt = s.CreatedAt 165 166 name, err := csettings.PublicName(inst) 167 if err != nil { 168 return err 169 } 170 email, err := inst.SettingsEMail() 171 if err != nil { 172 return err 173 } 174 175 s.Members = make([]Member, 1) 176 s.Members[0].Status = MemberStatusOwner 177 s.Members[0].PublicName = name 178 s.Members[0].Email = email 179 s.Members[0].Instance = inst.PageURL("", nil) 180 181 return nil 182 } 183 184 // CreatePreviewPermissions creates the permissions doc for previewing this sharing, 185 // or updates it with the new codes if the document already exists 186 func (s *Sharing) CreatePreviewPermissions(inst *instance.Instance) (*permission.Permission, error) { 187 doc, _ := permission.GetForSharePreview(inst, s.SID) 188 189 codes := make(map[string]string, len(s.Members)-1) 190 shortcodes := make(map[string]string, len(s.Members)-1) 191 192 for i, m := range s.Members { 193 if i == 0 { 194 continue 195 } 196 var err error 197 var previousCode, previousShort string 198 var okCode, okShort bool 199 key := m.Email 200 if key == "" { 201 key = m.Instance 202 } 203 if key == "" { 204 key = keyFromMemberIndex(i) 205 } 206 207 // Checks that we don't already have a sharing code 208 if doc != nil { 209 previousCode, okCode = doc.Codes[key] 210 previousShort, okShort = doc.ShortCodes[key] 211 } 212 213 if !okCode { 214 codes[key], err = inst.CreateShareCode(key) 215 if err != nil { 216 return nil, err 217 } 218 } else { 219 codes[key] = previousCode 220 } 221 if !okShort { 222 shortcodes[key] = crypto.GenerateRandomString(consts.ShortCodeLen) 223 } else { 224 shortcodes[key] = previousShort 225 } 226 } 227 228 set := make(permission.Set, len(s.Rules)) 229 getVerb := permission.VerbSplit("GET") 230 for i, rule := range s.Rules { 231 set[i] = permission.Rule{ 232 Type: rule.DocType, 233 Title: rule.Title, 234 Verbs: getVerb, 235 Selector: rule.Selector, 236 Values: rule.Values, 237 } 238 } 239 240 if doc == nil { 241 md := metadata.New() 242 md.CreatedByApp = s.AppSlug 243 subdoc := permission.Permission{ 244 Permissions: set, 245 Metadata: md, 246 } 247 return permission.CreateSharePreviewSet(inst, s.SID, codes, shortcodes, subdoc) 248 } 249 250 if doc.Metadata != nil { 251 err := doc.Metadata.UpdatedByApp(s.AppSlug, "") 252 if err != nil { 253 return nil, err 254 } 255 } 256 doc.Codes = codes 257 doc.ShortCodes = shortcodes 258 if err := couchdb.UpdateDoc(inst, doc); err != nil { 259 return nil, err 260 } 261 return doc, nil 262 } 263 264 func keyFromMemberIndex(index int) string { 265 return fmt.Sprintf("index:%d", index) 266 } 267 268 // GetInteractCode returns a sharecode that can be used for reading and writing 269 // the file. It uses a share-interact token. 270 func (s *Sharing) GetInteractCode(inst *instance.Instance, member *Member, memberIndex int) (string, error) { 271 interact, err := permission.GetForShareInteract(inst, s.ID()) 272 if err != nil { 273 if couchdb.IsNotFoundError(err) { 274 return s.CreateInteractPermissions(inst, member) 275 } 276 return "", err 277 } 278 279 // Check if the sharing has not been revoked and accepted again, in which 280 // case, we need to update the permission set. 281 needUpdate := false 282 set := s.CreateInteractSet() 283 if !set.HasSameRules(interact.Permissions) { 284 interact.Permissions = set 285 needUpdate = true 286 } 287 288 // If we already have a code for this member, let's use it 289 indexKey := keyFromMemberIndex(memberIndex) 290 for key, code := range interact.Codes { 291 if key == "" { 292 continue 293 } 294 if key == member.Instance || key == member.Email || key == indexKey { 295 if needUpdate { 296 if err := couchdb.UpdateDoc(inst, interact); err != nil { 297 return "", err 298 } 299 } 300 return code, nil 301 } 302 } 303 304 // Else, create a code and add it to the permission doc 305 key := member.Email 306 if key == "" { 307 key = member.Instance 308 } 309 if key == "" { 310 key = indexKey 311 } 312 code, err := inst.CreateShareCode(key) 313 if err != nil { 314 return "", err 315 } 316 interact.Codes[key] = code 317 if err := couchdb.UpdateDoc(inst, interact); err != nil { 318 return "", err 319 } 320 return code, nil 321 } 322 323 // CreateInteractPermissions creates the permissions doc for reading and 324 // writing a note inside this sharing. 325 func (s *Sharing) CreateInteractPermissions(inst *instance.Instance, m *Member) (string, error) { 326 key := m.Email 327 if key == "" { 328 key = m.Instance 329 } 330 code, err := inst.CreateShareCode(key) 331 if err != nil { 332 return "", err 333 } 334 codes := map[string]string{key: code} 335 set := s.CreateInteractSet() 336 337 md := metadata.New() 338 md.CreatedByApp = s.AppSlug 339 doc := permission.Permission{ 340 Permissions: set, 341 Metadata: md, 342 } 343 344 _, err = permission.CreateShareInteractSet(inst, s.SID, codes, doc) 345 if err != nil { 346 return "", err 347 } 348 return code, nil 349 } 350 351 // CreateInteractSet returns a set of permissions that can be used for 352 // share-interact. 353 func (s *Sharing) CreateInteractSet() permission.Set { 354 set := make(permission.Set, len(s.Rules)) 355 getVerb := permission.ALL 356 for i, rule := range s.Rules { 357 set[i] = permission.Rule{ 358 Type: rule.DocType, 359 Title: rule.Title, 360 Verbs: getVerb, 361 Selector: rule.Selector, 362 Values: rule.Values, 363 } 364 } 365 return set 366 } 367 368 // Create checks that the sharing is OK and it persists it in CouchDB if it is the case. 369 func (s *Sharing) Create(inst *instance.Instance) (*permission.Permission, error) { 370 if err := s.ValidateRules(); err != nil { 371 return nil, err 372 } 373 if len(s.Members) < 2 { 374 return nil, ErrNoRecipients 375 } 376 377 if err := couchdb.CreateDoc(inst, s); err != nil { 378 return nil, err 379 } 380 if rule := s.FirstFilesRule(); rule != nil && rule.Selector != couchdb.SelectorReferencedBy { 381 if err := s.AddReferenceForSharingDir(inst, rule); err != nil { 382 inst.Logger().WithNamespace("sharing"). 383 Warnf("Error on referenced_by for the sharing dir (%s): %s", s.SID, err) 384 } 385 } 386 387 if s.Owner && s.PreviewPath != "" { 388 return s.CreatePreviewPermissions(inst) 389 } 390 return nil, nil 391 } 392 393 // CreateRequest prepares a sharing as just a request that the user will have to 394 // accept before it does anything. 395 func (s *Sharing) CreateRequest(inst *instance.Instance) error { 396 if err := s.ValidateRules(); err != nil { 397 return err 398 } 399 if len(s.Members) < 2 { 400 return ErrNoRecipients 401 } 402 403 s.Active = false 404 s.Owner = false 405 s.UpdatedAt = time.Now() 406 s.Credentials = make([]Credentials, 1) 407 408 for i, m := range s.Members { 409 if m.Email != "" { 410 if c, err := contact.FindByEmail(inst, m.Email); err == nil { 411 s.Members[i].Name = c.PrimaryName() 412 } 413 } 414 } 415 416 err := couchdb.CreateNamedDocWithDB(inst, s) 417 if couchdb.IsConflictError(err) { 418 old, errb := FindSharing(inst, s.SID) 419 if errb != nil { 420 return errb 421 } 422 if old.Owner { 423 return ErrInvalidSharing 424 } 425 if old.Active { 426 return ErrAlreadyAccepted 427 } 428 s.ShortcutID = old.ShortcutID 429 s.SRev = old.SRev 430 err = couchdb.UpdateDoc(inst, s) 431 } 432 return err 433 } 434 435 // Revoke remove the credentials for all members, contact them, removes the 436 // triggers and set the active flag to false. 437 func (s *Sharing) Revoke(inst *instance.Instance) error { 438 var errm error 439 440 if !s.Owner { 441 return ErrInvalidSharing 442 } 443 for i := range s.Credentials { 444 if err := s.RevokeMember(inst, i+1); err != nil { 445 errm = multierror.Append(errm, err) 446 } 447 if err := s.ClearLastSequenceNumbers(inst, &s.Members[i+1]); err != nil { 448 return err 449 } 450 } 451 if err := s.RemoveTriggers(inst); err != nil { 452 return err 453 } 454 if err := RemoveSharedRefs(inst, s.SID); err != nil { 455 return err 456 } 457 if s.PreviewPath != "" { 458 if err := s.RevokePreviewPermissions(inst); err != nil { 459 return err 460 } 461 } 462 if rule := s.FirstBitwardenOrganizationRule(); rule != nil && len(rule.Values) > 0 { 463 if err := s.RemoveAllBitwardenMembers(inst, rule.Values[0]); err != nil { 464 return err 465 } 466 } 467 s.Active = false 468 if err := couchdb.UpdateDoc(inst, s); err != nil { 469 return err 470 } 471 return errm 472 } 473 474 // RevokePreviewPermissions ensure that the permissions for the preview page 475 // are no longer valid. 476 func (s *Sharing) RevokePreviewPermissions(inst *instance.Instance) error { 477 perms, err := permission.GetForSharePreview(inst, s.SID) 478 if err != nil { 479 return err 480 } 481 now := time.Now() 482 perms.ExpiresAt = &now 483 return couchdb.UpdateDoc(inst, perms) 484 } 485 486 // RevokeRecipient revoke only one recipient on the sharer. After that, if the 487 // sharing has still at least one active member, we keep it as is. Else, we 488 // disable the sharing. 489 func (s *Sharing) RevokeRecipient(inst *instance.Instance, index int) error { 490 if !s.Owner { 491 return ErrInvalidSharing 492 } 493 if err := s.RevokeMember(inst, index); err != nil { 494 return err 495 } 496 m := &s.Members[index] 497 if err := s.ClearLastSequenceNumbers(inst, m); err != nil { 498 return err 499 } 500 if rule := s.FirstBitwardenOrganizationRule(); rule != nil && len(rule.Values) > 0 { 501 if err := s.RemoveBitwardenMember(inst, m, rule.Values[0]); err != nil { 502 return err 503 } 504 } 505 return s.NoMoreRecipient(inst) 506 } 507 508 // RevokeRecipientBySelf revoke the sharing on the recipient side 509 func (s *Sharing) RevokeRecipientBySelf(inst *instance.Instance, sharingDirTrashed bool) error { 510 if s.Owner || len(s.Members) == 0 { 511 return ErrInvalidSharing 512 } 513 if err := s.RevokeOwner(inst); err != nil { 514 return err 515 } 516 if err := s.RemoveTriggers(inst); err != nil { 517 return err 518 } 519 if err := s.ClearLastSequenceNumbers(inst, &s.Members[0]); err != nil { 520 return err 521 } 522 if err := RemoveSharedRefs(inst, s.SID); err != nil { 523 inst.Logger().WithNamespace("sharing"). 524 Warnf("RevokeRecipientBySelf failed to remove shared refs (%s)': %s", s.ID(), err) 525 } 526 if !sharingDirTrashed { 527 if err := s.FixRevokedNotes(inst); err != nil { 528 inst.Logger().WithNamespace("sharing"). 529 Warnf("RevokeRecipientBySelf failed to fix notes for revoked sharing %s: %s", s.ID(), err) 530 } 531 532 if rule := s.FirstFilesRule(); rule != nil && rule.Mime == "" { 533 if err := s.RemoveSharingDir(inst); err != nil { 534 inst.Logger().WithNamespace("sharing"). 535 Warnf("RevokeRecipientBySelf failed to delete dir %s: %s", s.ID(), err) 536 } 537 } 538 } 539 if rule := s.FirstBitwardenOrganizationRule(); rule != nil && len(rule.Values) > 0 { 540 if err := s.RemoveBitwardenOrganization(inst, rule.Values[0]); err != nil { 541 return err 542 } 543 } 544 s.Active = false 545 546 for i, m := range s.Members { 547 if i > 0 && m.Instance != "" { 548 s.Members[i].Status = MemberStatusRevoked 549 break 550 } 551 } 552 553 return couchdb.UpdateDoc(inst, s) 554 } 555 556 // RemoveTriggers remove all the triggers associated to this sharing 557 func (s *Sharing) RemoveTriggers(inst *instance.Instance) error { 558 if err := removeSharingTrigger(inst, s.Triggers.TrackID); err != nil { 559 return err 560 } 561 for _, id := range s.Triggers.TrackIDs { 562 if err := removeSharingTrigger(inst, id); err != nil { 563 return err 564 } 565 } 566 if err := removeSharingTrigger(inst, s.Triggers.ReplicateID); err != nil { 567 return err 568 } 569 if err := removeSharingTrigger(inst, s.Triggers.UploadID); err != nil { 570 return err 571 } 572 s.Triggers = Triggers{} 573 return nil 574 } 575 576 func removeSharingTrigger(inst *instance.Instance, triggerID string) error { 577 if triggerID != "" { 578 err := job.System().DeleteTrigger(inst, triggerID) 579 if err != nil && !errors.Is(err, job.ErrNotFoundTrigger) { 580 return err 581 } 582 } 583 return nil 584 } 585 586 // RemoveBitwardenOrganization remove the shared bitwarden organization and the 587 // ciphers inside it. It is called on the recipient instance when the sharing 588 // is revoked for them. 589 func (s *Sharing) RemoveBitwardenOrganization(inst *instance.Instance, orgID string) error { 590 org := &bitwarden.Organization{} 591 if err := couchdb.GetDoc(inst, consts.BitwardenOrganizations, orgID, org); err != nil { 592 if couchdb.IsNotFoundError(err) { 593 return nil 594 } 595 return err 596 } 597 return org.Delete(inst) 598 } 599 600 // RevokeByNotification is called on the recipient side, after a revocation 601 // performed by the sharer 602 func (s *Sharing) RevokeByNotification(inst *instance.Instance) error { 603 if s.Owner { 604 return ErrInvalidSharing 605 } 606 if err := DeleteOAuthClient(inst, &s.Members[0], &s.Credentials[0]); err != nil { 607 return err 608 } 609 if err := s.RemoveTriggers(inst); err != nil { 610 return err 611 } 612 if err := s.ClearLastSequenceNumbers(inst, &s.Members[0]); err != nil { 613 return err 614 } 615 if err := RemoveSharedRefs(inst, s.SID); err != nil { 616 return err 617 } 618 if err := s.FixRevokedNotes(inst); err != nil { 619 inst.Logger().WithNamespace("sharing"). 620 Warnf("RevokeByNotification failed to fix notes for revoked sharing %s: %s", s.ID(), err) 621 } 622 if rule := s.FirstFilesRule(); rule != nil && rule.Mime == "" { 623 if err := s.RemoveSharingDir(inst); err != nil { 624 return err 625 } 626 } 627 if rule := s.FirstBitwardenOrganizationRule(); rule != nil && len(rule.Values) > 0 { 628 if err := s.RemoveBitwardenOrganization(inst, rule.Values[0]); err != nil { 629 return err 630 } 631 } 632 633 var err error 634 for i := 0; i < 3; i++ { 635 s.Triggers = Triggers{} 636 s.Credentials = nil 637 s.Active = false 638 639 for i, m := range s.Members { 640 if i > 0 && m.Instance != "" { 641 s.Members[i].Status = MemberStatusRevoked 642 break 643 } 644 } 645 646 err := couchdb.UpdateDoc(inst, s) 647 if err == nil || !couchdb.IsConflictError(err) { 648 break 649 } 650 651 // In case of conflict (409 from CouchDB), reload the document and try again 652 if errb := couchdb.GetDoc(inst, consts.Sharings, s.ID(), s); errb != nil { 653 break 654 } 655 } 656 return err 657 } 658 659 // RevokeRecipientByNotification is called on the sharer side, after a 660 // revocation performed by the recipient 661 func (s *Sharing) RevokeRecipientByNotification(inst *instance.Instance, m *Member) error { 662 if !s.Owner { 663 return ErrInvalidSharing 664 } 665 c := s.FindCredentials(m) 666 if err := DeleteOAuthClient(inst, m, c); err != nil { 667 return err 668 } 669 if err := s.ClearLastSequenceNumbers(inst, m); err != nil { 670 return err 671 } 672 if rule := s.FirstBitwardenOrganizationRule(); rule != nil && len(rule.Values) > 0 { 673 if err := s.RemoveBitwardenMember(inst, m, rule.Values[0]); err != nil { 674 return err 675 } 676 } 677 m.Status = MemberStatusRevoked 678 *c = Credentials{} 679 680 return s.NoMoreRecipient(inst) 681 } 682 683 // NoMoreRecipient cleans up the sharing if there is no more active recipient 684 func (s *Sharing) NoMoreRecipient(inst *instance.Instance) error { 685 for _, m := range s.Members { 686 if m.Status == MemberStatusReady { 687 return couchdb.UpdateDoc(inst, s) 688 } 689 } 690 if err := s.RemoveTriggers(inst); err != nil { 691 return err 692 } 693 s.Active = false 694 if err := couchdb.UpdateDoc(inst, s); err != nil { 695 return err 696 } 697 return RemoveSharedRefs(inst, s.SID) 698 } 699 700 // FindSharing retrieves a sharing document from its ID 701 func FindSharing(db prefixer.Prefixer, sharingID string) (*Sharing, error) { 702 res := &Sharing{} 703 err := couchdb.GetDoc(db, consts.Sharings, sharingID, res) 704 if err != nil { 705 return nil, err 706 } 707 return res, nil 708 } 709 710 // FindSharings retrieves an array of sharing documents from their IDs 711 func FindSharings(db prefixer.Prefixer, sharingIDs []string) ([]*Sharing, error) { 712 req := &couchdb.AllDocsRequest{ 713 Keys: sharingIDs, 714 } 715 var res []*Sharing 716 err := couchdb.GetAllDocs(db, consts.Sharings, req, &res) 717 if err != nil { 718 return nil, err 719 } 720 return res, nil 721 } 722 723 // FindActive returns the list of active sharings. 724 func FindActive(db prefixer.Prefixer) ([]*Sharing, error) { 725 req := &couchdb.FindRequest{ 726 UseIndex: "active", 727 Selector: mango.Equal("active", true), 728 Limit: 1000, 729 } 730 var res []*Sharing 731 err := couchdb.FindDocs(db, consts.Sharings, req, &res) 732 if err != nil { 733 return nil, err 734 } 735 return res, nil 736 } 737 738 // GetSharingsByDocType returns all the sharings for the given doctype 739 func GetSharingsByDocType(inst *instance.Instance, docType string) (map[string]*Sharing, error) { 740 req := &couchdb.ViewRequest{ 741 Key: docType, 742 IncludeDocs: true, 743 } 744 var res couchdb.ViewResponse 745 err := couchdb.ExecView(inst, couchdb.SharingsByDocTypeView, req, &res) 746 if err != nil { 747 return nil, err 748 } 749 sharings := make(map[string]*Sharing, len(res.Rows)) 750 751 for _, row := range res.Rows { 752 var doc Sharing 753 err := json.Unmarshal(row.Doc, &doc) 754 if err != nil { 755 return nil, err 756 } 757 // Avoid duplicates, i.e. a set a rules having the same doctype 758 sID := row.Value.(string) 759 if _, ok := sharings[sID]; !ok { 760 sharings[sID] = &doc 761 } 762 } 763 return sharings, nil 764 } 765 766 func findIntentForRedirect(inst *instance.Instance, webapp *app.WebappManifest, doctype string) (*app.Intent, string) { 767 action := "SHARING" 768 if webapp != nil { 769 if intent := webapp.FindIntent(action, doctype); intent != nil { 770 return intent, webapp.Slug() 771 } 772 } 773 var mans []app.WebappManifest 774 err := couchdb.GetAllDocs(inst, consts.Apps, &couchdb.AllDocsRequest{}, &mans) 775 if err != nil { 776 return nil, "" 777 } 778 for _, man := range mans { 779 if intent := man.FindIntent(action, doctype); intent != nil { 780 return intent, man.Slug() 781 } 782 } 783 return nil, "" 784 } 785 786 // RedirectAfterAuthorizeURL returns the URL for the redirection after a user 787 // has authorized a sharing. 788 func (s *Sharing) RedirectAfterAuthorizeURL(inst *instance.Instance) *url.URL { 789 doctype := s.Rules[0].DocType 790 webapp, _ := app.GetWebappBySlug(inst, s.AppSlug) 791 792 if intent, slug := findIntentForRedirect(inst, webapp, doctype); intent != nil { 793 u := inst.SubDomain(slug) 794 parts := strings.SplitN(intent.Href, "#", 2) 795 if len(parts[0]) > 0 { 796 u.Path = parts[0] 797 } 798 if len(parts) == 2 && len(parts[1]) > 0 { 799 u.Fragment = parts[1] 800 } 801 u.RawQuery = "sharing=" + s.SID 802 return u 803 } 804 805 if webapp == nil { 806 return inst.DefaultRedirection() 807 } 808 u := inst.SubDomain(webapp.Slug()) 809 u.RawQuery = "sharing=" + s.SID 810 return u 811 } 812 813 // EndInitial is used to finish the initial sync phase of a sharing 814 func (s *Sharing) EndInitial(inst *instance.Instance) error { 815 if s.NbFiles == 0 { 816 return nil 817 } 818 s.NbFiles = 0 819 s.Initial = false 820 if err := couchdb.UpdateDoc(inst, s); err != nil { 821 return err 822 } 823 doc := couchdb.JSONDoc{ 824 Type: consts.SharingsInitialSync, 825 M: map[string]interface{}{"_id": s.SID}, 826 } 827 realtime.GetHub().Publish(inst, realtime.EventDelete, &doc, nil) 828 if rule := s.FirstFilesRule(); rule != nil && rule.Mime == "" { 829 if _, err := s.GetSharingDir(inst); err != nil { 830 return err 831 } 832 } 833 return nil 834 } 835 836 // GetSharecode returns a sharecode for the given client that can be used to 837 // preview the sharing. 838 func GetSharecode(inst *instance.Instance, sharingID, clientID string) (string, error) { 839 var s Sharing 840 if err := couchdb.GetDoc(inst, consts.Sharings, sharingID, &s); err != nil { 841 return "", err 842 } 843 member, err := s.FindMemberByInboundClientID(clientID) 844 if err != nil { 845 return "", err 846 } 847 preview, err := permission.GetForSharePreview(inst, sharingID) 848 if err != nil { 849 if couchdb.IsNotFoundError(err) { 850 preview, err = s.CreatePreviewPermissions(inst) 851 } 852 if err != nil { 853 return "", err 854 } 855 } 856 857 for key, code := range preview.ShortCodes { 858 if key == member.Instance || key == member.Email { 859 return code, nil 860 } 861 } 862 for key, code := range preview.Codes { 863 if key == member.Instance || key == member.Email { 864 return code, nil 865 } 866 } 867 return "", ErrMemberNotFound 868 } 869 870 var _ couchdb.Doc = &Sharing{} 871 872 // GetSharecodeFromShortcut returns the sharecode from the shortcut for this sharing. 873 func (s *Sharing) GetSharecodeFromShortcut(inst *instance.Instance) (string, error) { 874 key := []string{consts.Sharings, s.SID} 875 end := []string{key[0], key[1], couchdb.MaxString} 876 req := &couchdb.ViewRequest{ 877 StartKey: key, 878 EndKey: end, 879 IncludeDocs: true, 880 } 881 var res couchdb.ViewResponse 882 err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, req, &res) 883 if err != nil { 884 return "", ErrInternalServerError 885 } 886 if len(res.Rows) == 0 { 887 return "", ErrInvalidSharing 888 } 889 890 fs := inst.VFS() 891 file, err := fs.FileByID(res.Rows[0].ID) 892 if err != nil || file.Mime != consts.ShortcutMimeType { 893 return "", ErrInvalidSharing 894 } 895 f, err := fs.OpenFile(file) 896 if err != nil { 897 return "", ErrInternalServerError 898 } 899 defer f.Close() 900 var buf bytes.Buffer 901 _, err = buf.ReadFrom(f) 902 if err != nil { 903 return "", ErrInternalServerError 904 } 905 u, err := url.Parse(buf.String()) 906 if err != nil { 907 return "", ErrInternalServerError 908 } 909 code := u.Query().Get("sharecode") 910 if code == "" { 911 return "", ErrInvalidSharing 912 } 913 return code, nil 914 } 915 916 func (s *Sharing) cleanShortcutID(inst *instance.Instance) string { 917 if s.ShortcutID == "" { 918 return "" 919 } 920 921 var parentID string 922 fs := inst.VFS() 923 if file, err := fs.FileByID(s.ShortcutID); err == nil { 924 parentID = file.DirID 925 if err := fs.DestroyFile(file); err != nil { 926 return "" 927 } 928 } 929 s.ShortcutID = "" 930 _ = couchdb.UpdateDoc(inst, s) 931 return parentID 932 } 933 934 // GetPreviewURL asks the owner's Cozy the URL for previewing the sharing. 935 func (s *Sharing) GetPreviewURL(inst *instance.Instance, state string) (string, error) { 936 u, err := url.Parse(s.Members[0].Instance) 937 if s.Members[0].Instance == "" || err != nil { 938 return "", ErrInvalidSharing 939 } 940 body, err := json.Marshal(map[string]interface{}{"state": state}) 941 if err != nil { 942 return "", err 943 } 944 res, err := request.Req(&request.Options{ 945 Method: http.MethodPost, 946 Scheme: u.Scheme, 947 Domain: u.Host, 948 Path: "/sharings/" + s.SID + "/preview-url", 949 Headers: request.Headers{ 950 echo.HeaderAccept: echo.MIMEApplicationJSON, 951 echo.HeaderContentType: echo.MIMEApplicationJSON, 952 }, 953 Body: bytes.NewReader(body), 954 }) 955 if err != nil { 956 return "", err 957 } 958 defer res.Body.Close() 959 960 var data map[string]interface{} 961 if err = json.NewDecoder(res.Body).Decode(&data); err != nil { 962 return "", ErrRequestFailed 963 } 964 965 previewURL, ok := data["url"].(string) 966 if !ok || previewURL == "" { 967 return "", ErrRequestFailed 968 } 969 return previewURL, nil 970 } 971 972 // AddShortcut creates a shortcut for this sharing on the local instance. 973 func (s *Sharing) AddShortcut(inst *instance.Instance, state string) error { 974 previewURL, err := s.GetPreviewURL(inst, state) 975 if err != nil { 976 return err 977 } 978 return s.CreateShortcut(inst, previewURL, true) 979 } 980 981 // CountNewShortcuts returns the number of shortcuts to a sharing that have not 982 // been seen. 983 func CountNewShortcuts(inst *instance.Instance) (int, error) { 984 count := 0 985 perPage := 1000 986 list := make([]couchdb.JSONDoc, 0, perPage) 987 var bookmark string 988 for { 989 req := &couchdb.FindRequest{ 990 UseIndex: "by-sharing-status", 991 Selector: mango.Equal("metadata.sharing.status", "new"), 992 Limit: perPage, 993 Bookmark: bookmark, 994 } 995 res, err := couchdb.FindDocsRaw(inst, consts.Files, req, &list) 996 if err != nil { 997 return 0, err 998 } 999 count += len(list) 1000 if len(list) < perPage { 1001 return count, nil 1002 } 1003 bookmark = res.Bookmark 1004 } 1005 } 1006 1007 // SendPublicKey can be used to send the public key after it has been 1008 // created/changed to the sharing owners. 1009 func SendPublicKey(inst *instance.Instance, publicKey string) error { 1010 sharings, err := GetSharingsByDocType(inst, consts.BitwardenOrganizations) 1011 if err != nil { 1012 return err 1013 } 1014 var errm error 1015 for _, s := range sharings { 1016 if s.Owner || !s.Active || s.Credentials == nil { 1017 continue 1018 } 1019 if err := s.sendPublicKeyToOwner(inst, publicKey); err != nil { 1020 errm = multierror.Append(errm, err) 1021 } 1022 } 1023 return errm 1024 } 1025 1026 func (s *Sharing) sendPublicKeyToOwner(inst *instance.Instance, publicKey string) error { 1027 u, err := url.Parse(s.Members[0].Instance) 1028 if err != nil { 1029 return err 1030 } 1031 ac := APICredentials{ 1032 Bitwarden: &APIBitwarden{ 1033 UserID: inst.ID(), 1034 PublicKey: publicKey, 1035 }, 1036 } 1037 data, err := jsonapi.MarshalObject(&ac) 1038 if err != nil { 1039 return err 1040 } 1041 body, err := json.Marshal(jsonapi.Document{Data: &data}) 1042 if err != nil { 1043 return err 1044 } 1045 opts := &request.Options{ 1046 Method: http.MethodPost, 1047 Scheme: u.Scheme, 1048 Domain: u.Host, 1049 Path: "/sharings/" + s.SID + "/public-key", 1050 Headers: request.Headers{ 1051 echo.HeaderContentType: jsonapi.ContentType, 1052 echo.HeaderAuthorization: "Bearer " + s.Credentials[0].AccessToken.AccessToken, 1053 }, 1054 Body: bytes.NewReader(body), 1055 ParseError: ParseRequestError, 1056 } 1057 res, err := request.Req(opts) 1058 if res != nil && res.StatusCode/100 == 4 { 1059 res, err = RefreshToken(inst, err, s, &s.Members[0], &s.Credentials[0], opts, body) 1060 } 1061 if err != nil { 1062 return err 1063 } 1064 _, _ = io.Copy(io.Discard, res.Body) 1065 res.Body.Close() 1066 return nil 1067 } 1068 1069 // CheckSharings will scan all the io.cozy.sharings documents and check their 1070 // triggers and members/credentials. 1071 func CheckSharings(inst *instance.Instance, skipFSConsistency bool) ([]map[string]interface{}, error) { 1072 checks := []map[string]interface{}{} 1073 err := couchdb.ForeachDocs(inst, consts.Sharings, func(_ string, data json.RawMessage) error { 1074 s := &Sharing{} 1075 if err := json.Unmarshal(data, s); err != nil { 1076 return err 1077 } 1078 1079 if err := s.ValidateRules(); err != nil { 1080 checks = append(checks, map[string]interface{}{ 1081 "id": s.SID, 1082 "type": "invalid_rules", 1083 "error": err.Error(), 1084 }) 1085 return nil 1086 } 1087 1088 accepted := false 1089 for _, m := range s.Members { 1090 if m.Status == MemberStatusReady { 1091 accepted = true 1092 } 1093 } 1094 1095 membersChecks, validMembers := s.checkSharingMembers() 1096 checks = append(checks, membersChecks...) 1097 1098 triggersChecks := s.checkSharingTriggers(inst, accepted) 1099 checks = append(checks, triggersChecks...) 1100 1101 credentialsChecks := s.checkSharingCredentials() 1102 checks = append(checks, credentialsChecks...) 1103 1104 if len(membersChecks) == 0 && len(triggersChecks) == 0 && len(credentialsChecks) == 0 { 1105 if !s.Owner || !s.Active { 1106 return nil 1107 } 1108 1109 parentSharingID, err := findParentFileSharingID(inst, s) 1110 if err != nil { 1111 return err 1112 } else if parentSharingID != "" { 1113 checks = append(checks, map[string]interface{}{ 1114 "id": s.SID, 1115 "type": "sharing_in_sharing", 1116 "instance": inst.Domain, 1117 "parent_sharing": parentSharingID, 1118 }) 1119 return nil 1120 } 1121 1122 if s.Initial || s.ReadOnly() { 1123 return nil 1124 } 1125 1126 isSharingReady := false 1127 for _, m := range s.Members { 1128 if m.Status == MemberStatusReady { 1129 isSharingReady = true 1130 break 1131 } 1132 } 1133 if !isSharingReady { 1134 return nil 1135 } 1136 1137 rule := s.FirstFilesRule() 1138 if rule == nil { 1139 return nil 1140 } 1141 1142 ownerDocs, err := FindMatchingDocs(inst, *rule) 1143 if err != nil { 1144 checks = append(checks, map[string]interface{}{ 1145 "id": s.SID, 1146 "type": "missing_matching_docs_for_owner", 1147 "error": err.Error(), 1148 }) 1149 return nil 1150 } 1151 1152 for _, m := range validMembers { 1153 ms, err := FindSharing(m, s.ID()) 1154 if err != nil { 1155 checks = append(checks, map[string]interface{}{ 1156 "id": s.SID, 1157 "type": "missing_sharing_for_member", 1158 "member": m.Domain, 1159 "error": err.Error(), 1160 }) 1161 continue 1162 } 1163 1164 if !ms.Active { 1165 continue 1166 } 1167 1168 parentSharingID, err := findParentFileSharingID(m, ms) 1169 if err != nil { 1170 return err 1171 } else if parentSharingID != "" { 1172 checks = append(checks, map[string]interface{}{ 1173 "id": ms.SID, 1174 "type": "sharing_in_sharing", 1175 "instance": m.Domain, 1176 "parent_sharing": parentSharingID, 1177 }) 1178 continue 1179 } 1180 1181 if !skipFSConsistency { 1182 checks = append(checks, s.checkSharingTreesConsistency(inst, ownerDocs, m, ms)...) 1183 } 1184 } 1185 } 1186 1187 return nil 1188 }) 1189 return checks, err 1190 } 1191 1192 // findParentFileSharingID returns the first sharing found accepting the root of 1193 // the given sharing. 1194 // 1195 // Since have a sharing within another one will generate unexpected behavior, 1196 // the goal is to find these situations. 1197 func findParentFileSharingID(inst *instance.Instance, sharing *Sharing) (string, error) { 1198 // Inactive sharings are not an issue so we skip them 1199 if !sharing.Active { 1200 return "", nil 1201 } 1202 1203 // 1. Get all root files for the sharing being checked 1204 sharingRule := sharing.FirstFilesRule() 1205 if sharingRule == nil { 1206 return "", nil 1207 } 1208 1209 var sharingRoots []couchdb.JSONDoc 1210 for _, id := range sharingRule.Values { 1211 var sharingRoot couchdb.JSONDoc 1212 if err := couchdb.GetDoc(inst, consts.Files, id, &sharingRoot); err != nil { 1213 // We can ignore the error here. It will be reported as 1214 // missing_matching_docs_for_owner or missing_matching_docs_for_member 1215 // later. 1216 return "", nil 1217 } 1218 sharingRoots = append(sharingRoots, sharingRoot) 1219 } 1220 1221 // 2. Get all other file sharings on inst 1222 fileSharings, err := GetSharingsByDocType(inst, consts.Files) 1223 if err != nil { 1224 return "", err 1225 } 1226 1227 var sharingIDs []string 1228 for _, fileSharing := range fileSharings { 1229 // Do not add sharing in sharing error for inactive sharings or the 1230 // sharing currently checked. 1231 if !fileSharing.Active || fileSharing.ID() == sharing.ID() { 1232 continue 1233 } 1234 1235 sharingIDs = append(sharingIDs, fileSharing.ID()) 1236 } 1237 1238 sharedDocsBySharingID, err := GetSharedDocsBySharingIDs(inst, sharingIDs) 1239 if err != nil { 1240 return "", err 1241 } 1242 1243 // 3. Check if one of the shared roots is part of another sharing 1244 for _, sharedRoot := range sharingRoots { 1245 for sid, sharedDocs := range sharedDocsBySharingID { 1246 for _, sharedDoc := range sharedDocs { 1247 if sharedRoot.ID() == sharedDoc.ID { 1248 return sid, nil 1249 } 1250 } 1251 } 1252 } 1253 1254 return "", nil 1255 } 1256 1257 func (s *Sharing) checkSharingTriggers(inst *instance.Instance, accepted bool) (checks []map[string]interface{}) { 1258 if s.Active && accepted { 1259 if s.Triggers.TrackID == "" && len(s.Triggers.TrackIDs) == 0 { 1260 checks = append(checks, map[string]interface{}{ 1261 "id": s.SID, 1262 "type": "missing_trigger_on_active_sharing", 1263 "trigger": "track", 1264 }) 1265 } else if s.Triggers.TrackID != "" { 1266 err := couchdb.GetDoc(inst, consts.Triggers, s.Triggers.TrackID, nil) 1267 if couchdb.IsNotFoundError(err) { 1268 checks = append(checks, map[string]interface{}{ 1269 "id": s.SID, 1270 "type": "missing_trigger_on_active_sharing", 1271 "trigger": "track", 1272 "trigger_id": s.Triggers.TrackID, 1273 }) 1274 } 1275 } else { 1276 for _, id := range s.Triggers.TrackIDs { 1277 err := couchdb.GetDoc(inst, consts.Triggers, id, nil) 1278 if couchdb.IsNotFoundError(err) { 1279 checks = append(checks, map[string]interface{}{ 1280 "id": s.SID, 1281 "type": "missing_trigger_on_active_sharing", 1282 "trigger": "track", 1283 "trigger_id": id, 1284 }) 1285 } 1286 } 1287 } 1288 1289 if s.Owner || !s.ReadOnly() { 1290 if s.Triggers.ReplicateID == "" { 1291 checks = append(checks, map[string]interface{}{ 1292 "id": s.SID, 1293 "type": "missing_trigger_on_active_sharing", 1294 "trigger": "replicate", 1295 }) 1296 } else { 1297 err := couchdb.GetDoc(inst, consts.Triggers, s.Triggers.ReplicateID, nil) 1298 if couchdb.IsNotFoundError(err) { 1299 checks = append(checks, map[string]interface{}{ 1300 "id": s.SID, 1301 "type": "missing_trigger_on_active_sharing", 1302 "trigger": "replicate", 1303 "trigger_id": s.Triggers.ReplicateID, 1304 }) 1305 } 1306 } 1307 1308 if s.FirstFilesRule() != nil { 1309 if s.Triggers.UploadID == "" { 1310 checks = append(checks, map[string]interface{}{ 1311 "id": s.SID, 1312 "type": "missing_trigger_on_active_sharing", 1313 "trigger": "upload", 1314 }) 1315 } else { 1316 err := couchdb.GetDoc(inst, consts.Triggers, s.Triggers.UploadID, nil) 1317 if couchdb.IsNotFoundError(err) { 1318 checks = append(checks, map[string]interface{}{ 1319 "id": s.SID, 1320 "type": "missing_trigger_on_active_sharing", 1321 "trigger": "upload", 1322 "trigger_id": s.Triggers.UploadID, 1323 }) 1324 } 1325 } 1326 } 1327 } 1328 } else { 1329 if s.Triggers.TrackID != "" || len(s.Triggers.TrackIDs) > 0 { 1330 id := s.Triggers.TrackID 1331 if id == "" { 1332 id = s.Triggers.TrackIDs[0] 1333 } 1334 checks = append(checks, map[string]interface{}{ 1335 "id": s.SID, 1336 "type": "trigger_on_inactive_sharing", 1337 "trigger": "track", 1338 "trigger_id": id, 1339 }) 1340 } 1341 if s.Triggers.ReplicateID != "" { 1342 checks = append(checks, map[string]interface{}{ 1343 "id": s.SID, 1344 "type": "trigger_on_inactive_sharing", 1345 "trigger": "replicate", 1346 "trigger_id": s.Triggers.ReplicateID, 1347 }) 1348 } 1349 if s.Triggers.UploadID != "" { 1350 checks = append(checks, map[string]interface{}{ 1351 "id": s.SID, 1352 "type": "trigger_on_inactive_sharing", 1353 "trigger": "upload", 1354 "trigger_id": s.Triggers.UploadID, 1355 }) 1356 } 1357 } 1358 1359 return checks 1360 } 1361 1362 func (s *Sharing) checkSharingMembers() (checks []map[string]interface{}, validMembers []*instance.Instance) { 1363 if len(s.Members) < 2 { 1364 checks = append(checks, map[string]interface{}{ 1365 "id": s.SID, 1366 "type": "not_enough_members", 1367 "nb_members": len(s.Members), 1368 }) 1369 return checks, nil 1370 } 1371 1372 var ownerDomain string 1373 for i, m := range s.Members { 1374 if m.Status == MemberStatusRevoked && !s.Active { 1375 continue 1376 } 1377 1378 isFirst := i == 0 1379 isOwner := m.Status == MemberStatusOwner 1380 1381 if isFirst != isOwner { 1382 checks = append(checks, map[string]interface{}{ 1383 "id": s.SID, 1384 "type": "invalid_member_status", 1385 "member": i, 1386 "status": m.Status, 1387 }) 1388 } 1389 1390 if isOwner { 1391 ownerDomain = strings.SplitN(m.Instance, ".", 2)[1] 1392 } 1393 } 1394 1395 for _, m := range s.Members { 1396 if m.Status == MemberStatusMailNotSent { 1397 if len(m.Groups) == 0 { 1398 checks = append(checks, map[string]interface{}{ 1399 "id": s.SID, 1400 "type": "mail_not_sent", 1401 "member": m.Instance, 1402 }) 1403 } 1404 continue 1405 } 1406 1407 if m.Status != MemberStatusReady { 1408 continue 1409 } 1410 1411 if !s.Owner && m.Instance == "" { 1412 continue 1413 } 1414 1415 u, err := url.ParseRequestURI(m.Instance) 1416 if err != nil { 1417 checks = append(checks, map[string]interface{}{ 1418 "id": s.SID, 1419 "type": "invalid_instance_for_member", 1420 "member": m.Instance, 1421 }) 1422 continue 1423 } 1424 1425 domain := strings.ToLower(u.Hostname()) 1426 if u.Port() != "" { 1427 domain += ":" + u.Port() 1428 } 1429 1430 member, err := instance.Get(domain) 1431 if err != nil { 1432 // If the member's instance cannot be found and doesn't share the 1433 // owner's instance domain, they're probably on different 1434 // environments so we simply skip this member. 1435 if !strings.HasSuffix(m.Instance, ownerDomain) { 1436 continue 1437 } 1438 1439 checks = append(checks, map[string]interface{}{ 1440 "id": s.SID, 1441 "type": "missing_instance_for_member", 1442 "member": domain, 1443 }) 1444 continue 1445 } 1446 1447 validMembers = append(validMembers, member) 1448 } 1449 1450 return checks, validMembers 1451 } 1452 1453 func (s *Sharing) checkSharingCredentials() (checks []map[string]interface{}) { 1454 if !s.Active { 1455 return checks 1456 } 1457 1458 if s.Owner { 1459 for i, m := range s.Members { 1460 if i == 0 || m.Status != MemberStatusReady { 1461 continue 1462 } 1463 if s.Credentials[i-1].Client == nil { 1464 checks = append(checks, map[string]interface{}{ 1465 "id": s.SID, 1466 "type": "missing_oauth_client", 1467 "member": i, 1468 "owner": true, 1469 }) 1470 } 1471 if s.Credentials[i-1].AccessToken == nil { 1472 checks = append(checks, map[string]interface{}{ 1473 "id": s.SID, 1474 "type": "missing_access_token", 1475 "member": i, 1476 "owner": true, 1477 }) 1478 } 1479 if m.Instance == "" { 1480 checks = append(checks, map[string]interface{}{ 1481 "id": s.SID, 1482 "type": "missing_instance_for_member", 1483 "member": i, 1484 }) 1485 } 1486 } 1487 1488 if len(s.Credentials)+1 != len(s.Members) { 1489 checks = append(checks, map[string]interface{}{ 1490 "id": s.SID, 1491 "type": "invalid_number_of_credentials", 1492 "owner": true, 1493 "nb_members": len(s.Credentials), 1494 }) 1495 return checks 1496 } 1497 } else { 1498 if len(s.Credentials) != 1 { 1499 checks = append(checks, map[string]interface{}{ 1500 "id": s.SID, 1501 "type": "invalid_number_of_credentials", 1502 "owner": false, 1503 "nb_members": len(s.Credentials), 1504 }) 1505 return checks 1506 } 1507 1508 if s.Credentials[0].InboundClientID == "" { 1509 checks = append(checks, map[string]interface{}{ 1510 "id": s.SID, 1511 "type": "missing_inbound_client_id", 1512 "owner": false, 1513 }) 1514 } 1515 1516 if !s.ReadOnly() && s.Members[0].Instance == "" { 1517 checks = append(checks, map[string]interface{}{ 1518 "id": s.SID, 1519 "type": "missing_instance_for_member", 1520 "member": 0, 1521 }) 1522 } 1523 } 1524 1525 return checks 1526 } 1527 1528 func (s *Sharing) checkSharingTreesConsistency(inst *instance.Instance, ownerDocs []couchdb.JSONDoc, m *instance.Instance, ms *Sharing) (checks []map[string]interface{}) { 1529 // We checked earlier that this rule exists 1530 ownerRule := s.FirstFilesRule() 1531 1532 memberRule := ms.FirstFilesRule() 1533 if memberRule == nil { 1534 checks = append(checks, map[string]interface{}{ 1535 "id": s.SID, 1536 "type": "missing_files_rule_for_member", 1537 "member": m.Domain, 1538 }) 1539 return checks 1540 } 1541 1542 memberDocs, err := FindMatchingDocs(m, *memberRule) 1543 if err != nil { 1544 checks = append(checks, map[string]interface{}{ 1545 "id": s.SID, 1546 "type": "missing_matching_docs_for_member", 1547 "member": m.Domain, 1548 "error": err.Error(), 1549 }) 1550 return checks 1551 } 1552 1553 if len(ms.Credentials) != 1 { 1554 checks = append(checks, map[string]interface{}{ 1555 "id": s.SID, 1556 "type": "invalid_number_of_credentials", 1557 "instance": m.Domain, 1558 "nb_members": len(ms.Credentials), 1559 }) 1560 return checks 1561 } 1562 1563 // Build a map of owner docs with their member's counterpart ids 1564 ownerKey := ms.Credentials[0].XorKey 1565 ownerDocsByID := make(map[string]couchdb.JSONDoc) 1566 for _, doc := range ownerDocs { 1567 ownerDocsByID[doc.ID()] = doc 1568 } 1569 1570 for _, memberDoc := range memberDocs { 1571 ownerID := XorID(memberDoc.ID(), ownerKey) 1572 1573 if ownerDoc, found := ownerDocsByID[ownerID]; found { 1574 if ownerDoc.Rev() != memberDoc.Rev() { 1575 if revision.Generation(ownerDoc.Rev()) < revision.Generation(memberDoc.Rev()) && ms.ReadOnly() { 1576 checks = append(checks, map[string]interface{}{ 1577 "id": s.SID, 1578 "type": "read_only_member", 1579 "member": m.Domain, 1580 }) 1581 } else if wasUpdatedRecently(ownerDoc) || wasUpdatedRecently(memberDoc) { 1582 // If the latest change happened less than 5 minutes ago, we'll 1583 // assume the sharing synchronization is still in progress and 1584 // that would explain the difference between the 2 revisions. 1585 // In this case, we do nothing. 1586 } else if revision.Generation(ownerDoc.Rev()) > revision.Generation(memberDoc.Rev()) && isFileTooBigForInstance(m, ownerDoc) { 1587 checks = append(checks, map[string]interface{}{ 1588 "id": s.SID, 1589 "type": "disk_quota_exceeded", 1590 "instance": m.Domain, 1591 "file": ownerDoc, 1592 }) 1593 } else if revision.Generation(ownerDoc.Rev()) < revision.Generation(memberDoc.Rev()) && isFileTooBigForInstance(inst, memberDoc) { 1594 checks = append(checks, map[string]interface{}{ 1595 "id": s.SID, 1596 "type": "disk_quota_exceeded", 1597 "instance": inst.Domain, 1598 "file": memberDoc, 1599 }) 1600 } else { 1601 checks = append(checks, map[string]interface{}{ 1602 "id": s.SID, 1603 "type": "invalid_doc_rev", 1604 "member": m.Domain, 1605 "ownerDoc": ownerDoc, 1606 "memberRev": memberDoc.Rev(), 1607 "memberID": memberDoc.ID(), 1608 }) 1609 } 1610 } else { 1611 // It's unnecessary to run these checks if both docs don't 1612 // have the same revision in the first place. 1613 1614 if ownerDoc.M["name"] != memberDoc.M["name"] { 1615 checks = append(checks, map[string]interface{}{ 1616 "id": s.SID, 1617 "type": "invalid_doc_name", 1618 "member": m.Domain, 1619 "ownerDoc": ownerDoc, 1620 "memberName": memberDoc.M["name"], 1621 "memberRev": memberDoc.Rev(), 1622 "memberID": memberDoc.ID(), 1623 }) 1624 } 1625 1626 if ownerDoc.M["type"] == consts.FileType && ownerDoc.M["checksum"] != memberDoc.M["checksum"] { 1627 checks = append(checks, map[string]interface{}{ 1628 "id": s.SID, 1629 "type": "invalid_doc_checksum", 1630 "member": m.Domain, 1631 "ownerDoc": ownerDoc, 1632 "memberChecksum": memberDoc.M["checksum"], 1633 "memberRev": memberDoc.Rev(), 1634 "memberID": memberDoc.ID(), 1635 }) 1636 } 1637 1638 isSharingRoot := false 1639 for _, v := range ownerRule.Values { 1640 if ownerDoc.ID() == v { 1641 isSharingRoot = true 1642 break 1643 } 1644 } 1645 1646 // Sharing roots are expected not to have the same parent 1647 if !isSharingRoot { 1648 memberDirID := memberDoc.M["dir_id"].(string) 1649 ownerDirID := ownerDoc.M["dir_id"].(string) 1650 if ownerDirID != XorID(memberDirID, ownerKey) { 1651 checks = append(checks, map[string]interface{}{ 1652 "id": s.SID, 1653 "type": "invalid_doc_parent", 1654 "member": m.Domain, 1655 "ownerDoc": ownerDoc, 1656 "memberParent": memberDirID, 1657 "memberRev": memberDoc.Rev(), 1658 "memberID": memberDoc.ID(), 1659 }) 1660 } 1661 } 1662 } 1663 1664 delete(ownerDocsByID, ownerID) 1665 } else { 1666 if ms.ReadOnly() { 1667 checks = append(checks, map[string]interface{}{ 1668 "id": s.SID, 1669 "type": "read_only_member", 1670 "member": m.Domain, 1671 }) 1672 continue 1673 } 1674 1675 if wasUpdatedRecently(memberDoc) { 1676 // If the document was created less than 5 minutes ago, we'll 1677 // assume the sharing synchronization is still in progress and 1678 // that would explain why it's missing on the other instance. 1679 // In this case, we do nothing. 1680 continue 1681 } 1682 1683 if isFileTooBigForInstance(inst, memberDoc) { 1684 checks = append(checks, map[string]interface{}{ 1685 "id": s.SID, 1686 "type": "disk_quota_exceeded", 1687 "instance": inst.Domain, 1688 "file": memberDoc, 1689 }) 1690 continue 1691 } 1692 1693 checks = append(checks, map[string]interface{}{ 1694 "id": s.SID, 1695 "type": "missing_matching_doc_for_owner", 1696 "member": m.Domain, 1697 "missing": memberDoc, 1698 }) 1699 } 1700 } 1701 1702 // The only docs left in the map do not exist on the member's instance 1703 for _, ownerDoc := range ownerDocsByID { 1704 if wasUpdatedRecently(ownerDoc) { 1705 // If the document was created less than 5 minutes ago, we'll 1706 // assume the sharing synchronization is still in progress and 1707 // that would explain why it's missing on the other instance. 1708 // In this case, we do nothing. 1709 continue 1710 } 1711 1712 if isFileTooBigForInstance(m, ownerDoc) { 1713 checks = append(checks, map[string]interface{}{ 1714 "id": s.SID, 1715 "type": "disk_quota_exceeded", 1716 "instance": m.Domain, 1717 "file": ownerDoc, 1718 }) 1719 break 1720 } 1721 1722 checks = append(checks, map[string]interface{}{ 1723 "id": s.SID, 1724 "type": "missing_matching_doc_for_member", 1725 "member": m.Domain, 1726 "missing": ownerDoc, 1727 "ownerDocID": ownerDoc.ID(), 1728 }) 1729 } 1730 1731 return checks 1732 } 1733 1734 // isFileTooBigForInstance returns true if the given doc represents a file and 1735 // its size is greater than the available space on the given instance. 1736 // If said instance does not have any defined quota, it returns false. 1737 func isFileTooBigForInstance(inst *instance.Instance, doc couchdb.JSONDoc) bool { 1738 if docType, ok := doc.M["type"].(string); !ok || docType == "" || docType == consts.DirType { 1739 return false 1740 } 1741 1742 var file *vfs.FileDoc 1743 1744 fileJSON, err := json.Marshal(doc) 1745 if err != nil { 1746 return false 1747 } 1748 1749 if err := json.Unmarshal(fileJSON, &file); err != nil { 1750 return false 1751 } 1752 1753 _, _, _, err = vfs.CheckAvailableDiskSpace(inst.VFS(), file) 1754 return errors.Is(err, vfs.ErrFileTooBig) || errors.Is(err, vfs.ErrMaxFileSize) 1755 } 1756 1757 // wasUpdatedRecently returns true if the given document's latest update, given 1758 // by its `cozyMetadata.updatedAt` attribute, happened less than 5 minutes ago. 1759 // If the attribute is missing or does not represent a valid date, we consider 1760 // the latest update happened before that. 1761 func wasUpdatedRecently(doc couchdb.JSONDoc) bool { 1762 cozyMetadata, ok := doc.M["cozyMetadata"].(map[string]interface{}) 1763 if !ok || cozyMetadata == nil { 1764 return false 1765 } 1766 if updatedAt, ok := cozyMetadata["updatedAt"].(time.Time); ok { 1767 return time.Since(updatedAt) < 5*time.Minute 1768 } 1769 return false 1770 }