github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/member.go (about)

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