github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/sharing/member.go (about)

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