Skip to content

fix: preserve delegated sharing conflicts#4852

Open
shepilov wants to merge 3 commits into
masterfrom
fix/delegated-sharing-conflict
Open

fix: preserve delegated sharing conflicts#4852
shepilov wants to merge 3 commits into
masterfrom
fix/delegated-sharing-conflict

Conversation

@shepilov

@shepilov shepilov commented Jul 15, 2026

Copy link
Copy Markdown
Member

Bug

When a recipient of an open shared drive adds several members, the request can
partially succeed on the owner while returning a 500 to the initiating
recipient.

Observed staging sequence:

  • the recipient starts POST /sharings/:id/recipients
  • the owner receives POST /recipients/delegated
  • the owner creates Ivan's shortcut (201)
  • the owner creates Petr's shortcut (201)
  • Ivan's automatic acceptance succeeds (200)
  • Petr's /answer callback returns 404 (The member was not found) because
    the owner cannot yet match Petr's invitation state to a persisted
    credential/member
  • the owner's sharing update fails with a CouchDB 409 Document update conflict
  • the initiating recipient treats this business conflict as an authentication
    failure, refreshes the token, and replays the complete delegated request
  • the replay encounters the now-ready Ivan and returns 409 Sharing already accepted by this recipient
  • the delegated error is exposed to Drive as a nested 500 Unqualified error
  • Petr receives an email, but cannot access the shared files because the
    credential exchange did not complete

This leaves the sharing inconsistent between instances: the owner can show Petr
as pending while the initiating recipient shows him as revoked or does not
display him.

Root cause

The owner added members and credentials only in memory, then sent Cozy shortcut
requests before saving the sharing. A trusted recipient can auto-accept
synchronously while its shortcut request is still in progress and call the
owner's /answer endpoint. At that point, the new invitation state was not yet
visible in CouchDB. The state existed in Petr's shortcut and was sent back by
his Cozy, but ProcessAnswer could not find the matching entry in the owner's
persisted Sharing.Credentials, returned ErrMemberNotFound, and the HTTP
handler mapped it to 404.

The auto-accept callback could also update the sharing concurrently. The final
owner-side save then used a stale CouchDB revision and failed with a conflict.
When status persistence retried a conflict, it reloaded the latest document only
into a local pointer, leaving the caller-owned sharing stale.

Finally, SendDelegated retried every 4xx response after refreshing the token.
That is unsafe for this non-idempotent request: 404 and 409 are business
errors, not authentication errors, and replaying the batch can duplicate side
effects or replace the original failure with Sharing already accepted.

Fix

  • persist the complete delegated group, member, and credential-state batch
    before sending any shortcut
  • retry the owner-side CouchDB update on revision conflicts and return invitation
    states only from the successfully persisted attempt
  • treat an already-active recipient as an idempotent no-op and continue
    processing the rest of the batch
  • after a status-update conflict, reload the latest sharing, preserve statuses
    that already reached ready, apply the desired statuses, and synchronize the
    caller-owned sharing before retrying
  • replay delegated requests only for responses that can be resolved by
    refreshing routing/authentication information: 401, 403, and 410
  • preserve structured delegated 4xx errors instead of wrapping them in a
    generic 500
  • create invitation links only for newly persisted, non-empty invitation states,
    while keeping member synchronization independent from that state map
  • keep group-only members without an email or Cozy instance synchronized as
    mail-not-sent

The invitation-state response is keyed by the recipient's Cozy instance URL
(or email when no instance is available). It contains only the states created
and persisted by this request; it is not a complete state for every member of
the sharing.

API compatibility

No Drive client API change is required. The existing
POST /sharings/:sharing-id/recipients request and delegated response format are
unchanged. The change makes mixed batches idempotent and ensures the response is
returned only after the owner has durably stored the states needed by immediate
auto-accept callbacks.

Tests

Coverage includes:

  • a mixed batch containing an already-active member and new recipients
  • a synchronous Cozy auto-accept callback executed during shortcut creation,
    verifying that the owner can resolve the persisted state, the new member
    reaches ready, and the shared file is accessible
  • group members without an address being synchronized without an invitation
  • status persistence after a CouchDB conflict
  • delegated retry policy for 401, 403, 410, 404, and 409
  • preservation of structured errors returned before or after token refresh

The sharing API documentation now also states that adding an already-active
recipient is idempotent and that delegated credentials are persisted before
shortcuts are sent.

@shepilov
shepilov force-pushed the fix/delegated-sharing-conflict branch 2 times, most recently from e6ae3ca to 23e4a15 Compare July 16, 2026 09:19
@shepilov
shepilov force-pushed the fix/delegated-sharing-conflict branch from 23e4a15 to 67cd898 Compare July 16, 2026 09:20
@shepilov
shepilov marked this pull request as ready for review July 16, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant