Skip to content

Commit 74b678e

Browse files
committed
fix: avoid races during interactive drive acceptance
1 parent 49d21f6 commit 74b678e

File tree

5 files changed

+611
-261
lines changed

5 files changed

+611
-261
lines changed

model/sharing/oauth.go

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"github.com/labstack/echo/v4"
2929
)
3030

31+
const InteractiveSharingQueryParam = "interactive"
32+
3133
// CreateSharingRequest sends information about the sharing to the recipient's cozy
3234
func (m *Member) CreateSharingRequest(inst *instance.Instance, s *Sharing, c *Credentials, u *url.URL) error {
3335
if len(c.XorKey) == 0 {
@@ -232,6 +234,16 @@ func countFilesInDirectory(inst *instance.Instance, dir *vfs.DirDoc) (int, error
232234

233235
// RegisterCozyURL saves a new Cozy URL for a member
234236
func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL string) error {
237+
return s.registerCozyURL(inst, m, cozyURL, false)
238+
}
239+
240+
// RegisterCozyURLInteractive saves a Cozy URL discovered during an interactive
241+
// acceptance flow and marks the recipient request so auto-accept is skipped.
242+
func (s *Sharing) RegisterCozyURLInteractive(inst *instance.Instance, m *Member, cozyURL string) error {
243+
return s.registerCozyURL(inst, m, cozyURL, true)
244+
}
245+
246+
func (s *Sharing) registerCozyURL(inst *instance.Instance, m *Member, cozyURL string, interactive bool) error {
235247
if !s.Owner {
236248
return ErrInvalidSharing
237249
}
@@ -255,6 +267,11 @@ func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL st
255267
u.RawQuery = ""
256268
u.Fragment = ""
257269
m.Instance = u.String()
270+
if interactive {
271+
q := u.Query()
272+
q.Set(InteractiveSharingQueryParam, "true")
273+
u.RawQuery = q.Encode()
274+
}
258275

259276
creds := s.FindCredentials(m)
260277
if creds == nil {
@@ -382,6 +399,44 @@ func CreateAccessToken(inst *instance.Instance, cli *oauth.Client, sharingID str
382399
}, nil
383400
}
384401

402+
// updateSharingWithConflictRetry persists the sharing and resolves stale-revision
403+
// conflicts by reconciling the desired changes onto the latest revision.
404+
// reconcile can mutate latest for the next retry and should return true when the
405+
// latest revision already contains a usable final state and no write is needed.
406+
func updateSharingWithConflictRetry(
407+
inst *instance.Instance,
408+
s *Sharing,
409+
retries int,
410+
reconcile func(latest *Sharing) bool,
411+
) error {
412+
log := inst.Logger().WithNamespace("sharing")
413+
current := s
414+
for attempt := 0; ; attempt++ {
415+
err := couchdb.UpdateDoc(inst, current)
416+
if err == nil {
417+
*s = *current
418+
return nil
419+
}
420+
if !couchdb.IsConflictError(err) {
421+
return err
422+
}
423+
424+
latest, findErr := FindSharing(inst, s.SID)
425+
if findErr != nil {
426+
return err
427+
}
428+
if reconcile(latest) {
429+
*s = *latest
430+
return nil
431+
}
432+
if attempt >= retries {
433+
return err
434+
}
435+
log.Debugf("Conflict while updating sharing %s, retrying with latest revision (%d/%d)", s.SID, attempt+1, retries)
436+
current = latest
437+
}
438+
}
439+
385440
// SendAnswer says to the sharer's Cozy that the sharing has been accepted, and
386441
// materialize that by an exchange of credentials.
387442
func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error {
@@ -509,7 +564,19 @@ func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error {
509564
}
510565
}
511566

512-
return couchdb.UpdateDoc(inst, s)
567+
return updateSharingWithConflictRetry(inst, s, 1, func(latest *Sharing) bool {
568+
if latest.Active {
569+
return true
570+
}
571+
latest.Members = s.Members
572+
latest.Credentials = s.Credentials
573+
latest.Active = s.Active
574+
latest.Initial = s.Initial
575+
if s.ShortcutID != "" {
576+
latest.ShortcutID = s.ShortcutID
577+
}
578+
return false
579+
})
513580
}
514581

515582
// ProcessAnswer takes somes credentials and update the sharing with those.
@@ -561,22 +628,20 @@ func (s *Sharing) ProcessAnswer(inst *instance.Instance, creds *APICredentials)
561628
}
562629

563630
s.Active = true
564-
if err := couchdb.UpdateDoc(inst, s); err != nil {
565-
if !couchdb.IsConflictError(err) {
566-
return nil, err
567-
}
568-
// A conflict can occur when several users accept a sharing at
569-
// the same time, and we should just retry in that case
570-
s2, err2 := FindSharing(inst, s.SID)
571-
if err2 != nil {
572-
return nil, err
573-
}
574-
s2.Members[i+1] = s.Members[i+1]
575-
s2.Credentials[i] = s.Credentials[i]
576-
if err2 := couchdb.UpdateDoc(inst, s2); err2 != nil {
577-
return nil, err
631+
if err := updateSharingWithConflictRetry(inst, s, 1, func(latest *Sharing) bool {
632+
if latest.Members[i+1].Status == MemberStatusReady &&
633+
latest.Credentials[i].Client != nil &&
634+
latest.Credentials[i].AccessToken != nil {
635+
ac.Credentials.Client = latest.Credentials[i].Client
636+
ac.Credentials.AccessToken = latest.Credentials[i].AccessToken
637+
return true
578638
}
579-
s = s2
639+
latest.Members[i+1] = s.Members[i+1]
640+
latest.Credentials[i] = s.Credentials[i]
641+
latest.Active = s.Active
642+
return false
643+
}); err != nil {
644+
return nil, err
580645
}
581646
if creds.Bitwarden != nil {
582647
if err := s.SaveBitwarden(inst, &s.Members[i+1], creds.Bitwarden); err != nil {

web/oidc/oidc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ func acceptSharing(c echo.Context, ownerInst *instance.Instance, conf *Config, t
489489
return renderError(c, ownerInst, http.StatusBadRequest, "Sorry, you cannot accept your own sharing invitation.")
490490
}
491491
memberURL := memberInst.PageURL("/", nil)
492-
if err = s.RegisterCozyURL(ownerInst, member, memberURL); err != nil {
492+
if err = s.RegisterCozyURLInteractive(ownerInst, member, memberURL); err != nil {
493493
return renderError(c, ownerInst, http.StatusInternalServerError, "Sorry, the sharing invitation could not be processed.")
494494
}
495495
sharing.PersistInstanceURL(ownerInst, member.Email, member.Instance)

0 commit comments

Comments
 (0)