Skip to content

Conversation

@st3iny
Copy link
Member

@st3iny st3iny commented Jul 28, 2025

Summary

The beast ...

Terminology

  • Remote instance: Instance which is receiving a share.
  • Host instance: Instance which is owning/hosting the shared calendar.

Architecture

Sharing works by leveraging our on-instance sharing via CalDavBackend (and appending the _shared_by_<uid> suffix). The remote user is authenticated in Sabre through a "virtual" principal in principals/remote-users/<base64-encoded-cloud-id> with whom an outgoing federated calendar is shared "locally". The cloud id needs to be encoded as it might contain slashes, for example, when the remote instance is hosted inside a web root. The outgoing shared calendars are hosted inside a separate calendar root in remote-calendars/<base64-encoded-cloud-id>/<calendar_name>_shared_by_<sharer> on the host instance. The remote instance will send all sync requests to this address and authenticate using the remote user's cloud id and a shared secret (token) via HTTP basic auth.

A federation provider was implemented to send (and receive) shares across federated instances. The remote instance will track the incoming federated calendar inside a new table oc_calendars_federated. The host instance will create a regular DAV share inside oc_dav_shares in case the remote user logs in and syncs the calendar. I had to add a new column token to oc_dav_shares in order to track the auth token (shared secret) for the remote user (so that the sharer is able to revoke individual shared calendars).

If a calendar object is changed on the host instance, a cloud notification is sent to all remote instances with which the calendar is shared with. Remote servers will then queue a sync job at the next cron interval to sync the affected calendar. This also has the benefit of nicely debouncing the sync requests. As a backup, there is an hourly background job which syncs all remaining calendars (starting with the least recently synced calendar).

Please let me know in case there are questions or further elaboration is needed.

How to test?

  1. Set up two instances (host and remote).
  2. Install calendar on both instances.
  3. Calendar: Check out the linked PR above (at least on the remote instance).
  4. Server: Check out this branch on both instances.
  5. Federate both.
  6. Optional: Sync the federated address books from remote to host (via occ federation:sync-addressbooks) to make finding the remote users easier in the sharing search.
  7. Create a calendar with some events on the host instance.
  8. Share the calendar with the remote instance (type in the domain of the remote instance to quickly find remote users to share with).

Run the following command on the remote instance: occ federation:sync-calendars

Now, you should be able to see all the events in the calendar on the remote instance.

TODO

  • Remove a lot of weird leftover code
  • Either trigger the sync job immediately or sync directly on incoming shares
  • Fix existing tests
  • Add new tests
  • Chunk results (reuse Hamza's truncation?) -> deferred
  • Consider multi-get to bundle ICS download -> deferred
  • Resolve conflicts
  • Fix integration tests
  • Fix Psalm issues

Checklist

@st3iny st3iny added this to the Nextcloud 32 milestone Jul 28, 2025
@st3iny st3iny self-assigned this Jul 28, 2025
@st3iny st3iny added enhancement 2. developing Work in progress feature: federation feature: caldav Related to CalDAV internals labels Jul 28, 2025
@st3iny st3iny force-pushed the feat/federated-calendar-sharing branch from 41a55e7 to 47fd0be Compare July 30, 2025 11:49
@tcitworld
Copy link
Member

I get the architecture logic but having a one-hour sync interval between changes is quite a bumper, and in this case it's far more important for usability than the regular cached subscriptions sync interval.

Instead of going for a pull model, why not go for a push model? Queue a QueuedJob after each federated calendar change to propagate changes to every federated sharee, and only have pull as a fallback.

@st3iny st3iny force-pushed the feat/federated-calendar-sharing branch from 7b4c8da to 4a1252f Compare August 8, 2025 08:58
@st3iny
Copy link
Member Author

st3iny commented Aug 8, 2025

Instead of going for a pull model, why not go for a push model? Queue a QueuedJob after each federated calendar change to propagate changes to every federated sharee, and only have pull as a fallback.

That was my idea initially but then I ran out of time. We could use ICloudFederationNotification to achieve that and write a simple calendar event listener.

@st3iny
Copy link
Member Author

st3iny commented Aug 8, 2025

Done, I implemented a simple push sync to make the feature a bit more useful. The periodic job will pick up all other calendars.

@st3iny st3iny marked this pull request as ready for review August 12, 2025 12:10
@st3iny st3iny requested review from come-nc and removed request for a team August 12, 2025 12:10
@st3iny st3iny added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Aug 12, 2025
@st3iny
Copy link
Member Author

st3iny commented Aug 12, 2025

I'm sorry for creating this monster. Have fun reviewing ... 😅

@st3iny st3iny added the pending documentation This pull request needs an associated documentation update label Aug 12, 2025

$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
} elseif ($calendarType === self::CALENDAR_TYPE_FEDERATED) {
// TODO: implement custom event for federated calendars
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still todo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is still a TODO left for the future. I didn't implement the events as there is no use case for them yet.

@come-nc
Copy link
Contributor

come-nc commented Aug 14, 2025

Reviewed a tiny part for today, will continue next week, some random questions popping:

  • How consistent is it with federated file sharing?
  • It supports only users and not groups, right? Will it be "easy" to add groups later?
  • What about circles?

Copy link
Member

@ChristophWurst ChristophWurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense conceptually. Thanks a lot for the architecture overview

Code mostly makes sense. A bit much to review and process at a time 🙈

Copy link
Contributor

@SebastianKrupinski SebastianKrupinski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some other thought and things,

Not sure what the reasons was to hard wire the federation in to the DAV stack (CalendarHome, Provider and Root), I think doing this as a DAV Calendar Plugin would have been much simpler. I think it would have been more modular, easier to maintain and control but this not a blocker.

One thing I did find in my testing was that after unsharing the calendar all the calendar object are still present in the database, of the user the calendar was shared with

Comment on lines +70 to +75
if (isset($status[200])) {
$absoluteUrl = $this->prepareUri($url, $resource);
$vCard = $this->download($absoluteUrl, $username, $sharedSecret);
$this->atomic(function () use ($calendar, $objectUri, $vCard): void {
$existingObject = $this->backend->getCalendarObject($calendar->getId(), $objectUri, CalDavBackend::CALENDAR_TYPE_FEDERATED);
if (!$existingObject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a blocker, but more of a this might become a issue when syncing large calendars. I noticed while testing with a large calendar (12k) that if the process is interrupted, for what ever reason, the sync start back at the beginning, even after it already syncing 2k of entries.

Since syncing large calendars can take a long time (12k = 3hours on my machine) and causes thousands of requests this can get interrupted but firewall rate limits, slow connection, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is a problem. However, this has to be solved on the CalDAV protocol level as I didn't implement a custom one.

AFAIK, there is no way to continue an interrupted initial sync according to the RFCs.

@st3iny
Copy link
Member Author

st3iny commented Aug 21, 2025

* How consistent is it with federated file sharing?

Not at all because both behave very differently. Calendar federation will cause a local copy of all remote events to be synced. File federation will only request the data from the other instance if required. So I don't think they can be compared in a meaningful way.

* It supports only users and not groups, right? Will it be "easy" to add groups later?

Yes, for now it only supports users. Adding support for groups should be straight forward and not too much effort.

* What about circles?

I did not look into that. I have no idea about circle federation and can give no estimate on the work that would need to be done.

@nextcloud-bot nextcloud-bot mentioned this pull request Aug 22, 2025
@SebastianKrupinski
Copy link
Contributor

Tested again working good

@nextcloud-bot nextcloud-bot mentioned this pull request Aug 25, 2025
@ChristophWurst ChristophWurst added 4. to release Ready to be released and/or waiting for tests to finish and removed 3. to review Waiting for reviews labels Aug 26, 2025
@st3iny st3iny force-pushed the feat/federated-calendar-sharing branch from 4397af3 to f285ec9 Compare August 27, 2025 15:03
@st3iny
Copy link
Member Author

st3iny commented Aug 27, 2025

I rebased to fix conflicts. Should be all green now.

Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
@st3iny st3iny force-pushed the feat/federated-calendar-sharing branch from f285ec9 to b7dc720 Compare August 27, 2025 15:14
@AndyScherzinger AndyScherzinger merged commit 247b254 into master Aug 27, 2025
218 of 222 checks passed
@AndyScherzinger AndyScherzinger deleted the feat/federated-calendar-sharing branch August 27, 2025 17:23
@nextcloud-bot nextcloud-bot mentioned this pull request Aug 28, 2025
@skjnldsv skjnldsv modified the milestones: Nextcloud 32, Nextcloud 33 Sep 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4. to release Ready to be released and/or waiting for tests to finish enhancement feature: caldav Related to CalDAV internals feature: federation pending documentation This pull request needs an associated documentation update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read-only federated calendar shares

8 participants