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  }