Class CaldavCalendarShareService

java.lang.Object
org.exoplatform.caldav.service.CaldavCalendarShareService

@Service public class CaldavCalendarShareService extends Object
Shares a user's own eXo calendar, read-only, with colleagues connected to the same CalDAV server, by changing who may read the collection that calendar is bound to — through the server's own granting mechanism, confirmed by reading the access list back (EXO-90253).

Every check lives here, and none of them takes anything from the browser but a calendar id and a login:

  • the calendar exists and the caller owns it — a space calendar, owned by its space, is refused, and so is a colleague's;
  • the caller's account is connected, and the calendar is bound either to a collection eXo created for it — an SyncOrigin.EXO pair, active, whose collection carries the slug eXo derives from the calendar's own anchor — or to an imported collection the caller really owns: an active, anchored SyncOrigin.REMOTE pair that is not the meetings mirror, whose ownership is confirmed on the server before its access list is read or anything is changed (only the capability probe, which selects the rule, comes first) (on RFC 3744 servers its DAV:owner is the caller's recorded principal, it is writable for them and it sits under their calendar home; on BlueMind its path is under the caller's own uid, it is no subscription to someone else's calendar, and BlueMind's REST access list gives the caller All or Manage). The collection is resolved from the pair and never named by the request, so a share, a hidden share, the mirror or any other collection cannot be addressed; the client's CalDavClient.writeAcl(org.exoplatform.caldav.client.CalDavEndpoint, org.exoplatform.caldav.model.CalendarSync, java.util.List<org.exoplatform.caldav.client.AccessControlEntry>) applies the pair rule again;
  • the server offers a granting mechanism whose every change eXo confirms (SharingMechanism) — Stalwart's RFC 3744 ACL method, or BlueMind's CS:share confirmed through its REST API;
  • the sharee is an eXo user, not the caller, connected to the same server registration, whose principal was recorded by a discovery (EXO-90243) and is not the caller's own principal — two eXo users on one login would be "sharing" with themselves.

Read-modify-write, never overwrite. RFC 3744 §8.1 has an ACL request replace every entry that is neither protected nor inherited, and Stalwart replaces its grant list the same way. So a grant reads the list, keeps every modifiable entry — including entries eXo did not write, a share made from another client — adds one DAV:read grant, and writes the list back; a revoke removes that principal's read-only grants and nothing else. A list with an entry the client cannot represent is never written. Protected and inherited entries are never sent: the server keeps them.

What a list read over DAV cannot carry back. A server's access list over DAV is a projection of its own rights, and not a faithful one: on Stalwart (source, crates/dav/src/common/acl.rs and crates/jmap-proto/src/object/calendar.rs, main 474dd022) a colleague given "may delete" or "may write all" through JMAP reads back as DAV:write, and that entry written back becomes full write — eXo would widen somebody's rights by sharing with somebody else. Every widening is an entry reading back with more than the read-only privileges, so another principal holding any such entry stops the write (FOREIGN_ACCESS_NOT_PRESERVED), and the owner manages that calendar's access where it was set. What remains is narrowing: rights DAV does not show at all (JMAP "may write own", "may update private", "may RSVP"; the invite and reply parts of a scheduling grant) sit behind a read-only entry and are dropped when it is written back. That is accepted as the lesser loss, and stated here so it is a decision rather than a surprise.

Verified, not claimed. The list is read again after every write, and a grant absent from it — or a revoked grant still in it — is reported as not applied, whatever status the write answered.

Nothing is stored. Who a calendar is shared with is the server's state and anyone's other client can change it, so every listing reads it afresh. Concurrent edits by this node are serialised per collection; edits from another node or another client at the same moment are last writer wins, because RFC 3744 offers no conditional ACL request. Each applied grant and revoke is logged at info: who, to whom, which calendar.

On BlueMind (SharingMechanism.BLUEMIND_SHARE) the change is a POST CS:share naming the colleague by the mail address their own principal publishes, and the proof is the container's access list read back through BlueMind's REST API with the owner's credentials. A colleague sees the calendar only once they subscribed to it in BlueMind, which the listing says (subscriptionRequired).

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final record 
    A calendar's collection on the caller's server and the shares read from it, both from one resolution of the calendar (EXO-90385).
    static final record 
    A calendar's collection on the caller's server.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    The access list holds an entry eXo cannot write back faithfully.
    static final String
    The server would not let the caller read the calendar's access list.
    static final String
    The calendar does not exist, or was deleted.
    static final String
    The calendar is bound to no collection eXo can share: none eXo created for it, and no active imported one.
    static final String
    The server refused the account's stored credentials.
    static final String
    Another principal holds access beyond seeing the calendar, which writing the list back could change on the server.
    static final String
    The server accepted the change, and the list read back does not hold it.
    static final String
    The caller has no connected CalDAV account.
    static final String
    The imported collection is not the caller's own on the server: another principal owns it, it is read-only for them, it lies outside their calendar home, it is a subscription to someone else's calendar, or the server gives them no right to manage who sees it.
    static final String
    The caller does not own the calendar.
    static final String
    The sharee holds more than read access, granted outside eXo.
    static final String
    The server offers no way to grant access whose changes eXo can confirm.
    static final String
    Neither the server nor eXo's record says which principal the caller is connected as, so a colleague on the caller's own login cannot be told apart.
    static final String
    The sharee is connected to the server as the caller's own principal.
    static final String
    The server refused the change.
    static final String
    The server could not be reached or answered something unusable.
    static final String
    The server named no mail address for the sharee's principal, which is the only way BlueMind's share handler finds a sharee.
    static final String
    The colleague holds access below viewing given outside eXo — seeing when the owner is free, say — which a share from eXo would replace and a later stop would erase.
    static final String
    The sharee is the caller.
    static final String
    The sharee has no recorded identity on the same server.
    static final String
    No sharee was named.
    static final String
    The sharee is not a known, active eXo user.
  • Constructor Summary

    Constructors
    Constructor
    Description
    CaldavCalendarShareService(org.exoplatform.agenda.service.AgendaCalendarService agendaCalendarService, CaldavConnectorStorage caldavConnectorStorage, CaldavSyncStorage caldavSyncStorage, CalDavClient calDavClient, CaldavConnectionIdentityService caldavConnectionIdentityService, org.exoplatform.social.core.manager.IdentityManager identityManager, BlueMindAclClient blueMindAclClient, CaldavPushService caldavPushService, CaldavShareSubscriptionService caldavShareSubscriptionService, CaldavServerOwnerService caldavServerOwnerService, CaldavServerService caldavServerService, long probeMemoSeconds)
     
  • Method Summary

    Modifier and Type
    Method
    Description
    List<org.exoplatform.caldav.model.CalendarShares.ShareUser>
    candidates(long userIdentityId, String username, long calendarId, String query)
    The colleagues a calendar of the caller's can be shared with: eXo users connected to the same server registration, with a recorded principal that is not the caller's own.
    org.exoplatform.caldav.model.CalendarShares
    grant(long userIdentityId, String username, long calendarId, String shareeUsername)
    Gives one colleague read access to a calendar of the caller's.
    org.exoplatform.caldav.model.CalendarShares
    grant(long userIdentityId, String username, long calendarId, String shareeUsername, org.exoplatform.caldav.model.CalendarShares.ShareAccess access)
    Gives one colleague access to a calendar of the caller's, at the level asked for (EXO-90378).
    boolean
    holdsMeetingCopies(long userIdentityId, String username, long calendarId)
    Whether a calendar of the caller's, on their server, is where their eXo meeting copies are written (EXO-90357): what agenda's Share drawer asks before sharing it, through this add-on's channel plugin.
    org.exoplatform.caldav.model.CalendarShares
    listShares(long userIdentityId, String username, long calendarId)
    Who a calendar of the caller's is shared with, read from the server now.
    org.exoplatform.caldav.model.CalendarShares
    revoke(long userIdentityId, String username, long calendarId, String shareeUsername)
    Takes access to a calendar of the caller's away from one colleague, whichever level eXo granted it at.
    serverShares(long userIdentityId, String username, long calendarId)
    Who a calendar of the caller's is shared with, and the collection those shares live on, from one resolution of the calendar (EXO-90385).
    shareableCalendarIds(long userIdentityId, String username)
    The caller's calendars that can be shared from eXo: owned, bound to a collection eXo created or to an imported collection they own on the server, on a server offering a mechanism whose changes eXo can confirm.
    sharedCollectionOf(long userIdentityId, String username, long calendarId)
    The collection a calendar of the caller's is bound to on their server, and that server (EXO-90357): what agenda records on a share this add-on delivered, so the sharee's own listing of the server's collections can be told from it.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • CALENDAR_NOT_FOUND

      public static final String CALENDAR_NOT_FOUND
      The calendar does not exist, or was deleted.
      See Also:
    • NOT_OWNER

      public static final String NOT_OWNER
      The caller does not own the calendar.
      See Also:
    • NOT_CONNECTED

      public static final String NOT_CONNECTED
      The caller has no connected CalDAV account.
      See Also:
    • CALENDAR_NOT_ON_SERVER

      public static final String CALENDAR_NOT_ON_SERVER
      The calendar is bound to no collection eXo can share: none eXo created for it, and no active imported one.
      See Also:
    • NOT_OWNED_ON_SERVER

      public static final String NOT_OWNED_ON_SERVER
      The imported collection is not the caller's own on the server: another principal owns it, it is read-only for them, it lies outside their calendar home, it is a subscription to someone else's calendar, or the server gives them no right to manage who sees it.
      See Also:
    • NOT_SUPPORTED

      public static final String NOT_SUPPORTED
      The server offers no way to grant access whose changes eXo can confirm.
      See Also:
    • SHAREE_REQUIRED

      public static final String SHAREE_REQUIRED
      No sharee was named.
      See Also:
    • SHAREE_UNKNOWN

      public static final String SHAREE_UNKNOWN
      The sharee is not a known, active eXo user.
      See Also:
    • SHAREE_IS_OWNER

      public static final String SHAREE_IS_OWNER
      The sharee is the caller.
      See Also:
    • SHAREE_NOT_CONNECTED

      public static final String SHAREE_NOT_CONNECTED
      The sharee has no recorded identity on the same server.
      See Also:
    • SAME_PRINCIPAL

      public static final String SAME_PRINCIPAL
      The sharee is connected to the server as the caller's own principal.
      See Also:
    • NOT_READ_ONLY

      public static final String NOT_READ_ONLY
      The sharee holds more than read access, granted outside eXo.
      See Also:
    • ACL_UNREADABLE

      public static final String ACL_UNREADABLE
      The server would not let the caller read the calendar's access list.
      See Also:
    • ACL_NOT_UNDERSTOOD

      public static final String ACL_NOT_UNDERSTOOD
      The access list holds an entry eXo cannot write back faithfully.
      See Also:
    • SERVER_REFUSED

      public static final String SERVER_REFUSED
      The server refused the change.
      See Also:
    • OWNER_UNKNOWN

      public static final String OWNER_UNKNOWN
      Neither the server nor eXo's record says which principal the caller is connected as, so a colleague on the caller's own login cannot be told apart.
      See Also:
    • SHAREE_ADDRESS_UNKNOWN

      public static final String SHAREE_ADDRESS_UNKNOWN
      The server named no mail address for the sharee's principal, which is the only way BlueMind's share handler finds a sharee.
      See Also:
    • SHAREE_HAS_OTHER_ACCESS

      public static final String SHAREE_HAS_OTHER_ACCESS
      The colleague holds access below viewing given outside eXo — seeing when the owner is free, say — which a share from eXo would replace and a later stop would erase.
      See Also:
    • FOREIGN_ACCESS_NOT_PRESERVED

      public static final String FOREIGN_ACCESS_NOT_PRESERVED
      Another principal holds access beyond seeing the calendar, which writing the list back could change on the server.
      See Also:
    • NOT_APPLIED

      public static final String NOT_APPLIED
      The server accepted the change, and the list read back does not hold it.
      See Also:
    • SERVER_UNAVAILABLE

      public static final String SERVER_UNAVAILABLE
      The server could not be reached or answered something unusable.
      See Also:
    • CREDENTIALS

      public static final String CREDENTIALS
      The server refused the account's stored credentials.
      See Also:
  • Constructor Details

    • CaldavCalendarShareService

      @Autowired public CaldavCalendarShareService(org.exoplatform.agenda.service.AgendaCalendarService agendaCalendarService, CaldavConnectorStorage caldavConnectorStorage, CaldavSyncStorage caldavSyncStorage, CalDavClient calDavClient, CaldavConnectionIdentityService caldavConnectionIdentityService, org.exoplatform.social.core.manager.IdentityManager identityManager, BlueMindAclClient blueMindAclClient, CaldavPushService caldavPushService, CaldavShareSubscriptionService caldavShareSubscriptionService, CaldavServerOwnerService caldavServerOwnerService, CaldavServerService caldavServerService, @Value("${exo.caldav.share.probeMemoSeconds:300}") long probeMemoSeconds)
      Parameters:
      agendaCalendarService - where the calendar and its owner are read
      caldavConnectorStorage - the caller's connected account
      caldavSyncStorage - the pair binding the calendar to its collection
      calDavClient - the protocol
      caldavConnectionIdentityService - who each eXo user is on the server
      identityManager - the social identities of caller and sharees
      blueMindAclClient - reads a BlueMind calendar's access list back
      caldavPushService - says where the copies of eXo meetings are written
      caldavShareSubscriptionService - subscribes a BlueMind colleague to the calendar just shared with them, and unsubscribes them on a revoke (EXO-90277)
  • Method Details

    • shareableCalendarIds

      public List<Long> shareableCalendarIds(long userIdentityId, String username)
      The caller's calendars that can be shared from eXo: owned, bound to a collection eXo created or to an imported collection they own on the server, on a server offering a mechanism whose changes eXo can confirm.

      What decides whether agenda shows "Share" on a calendar, so it never fails: anything that goes wrong — no account, a server that cannot be reached, agenda failing — answers no calendar, and the entry is simply not offered. The server is asked what one such collection advertises — one OPTIONS, plus a depth-0 PROPFIND where that answer carries no DAV header, as on the BlueMind deployments observed — since what a server supports does not vary between two collections of one account in any server characterised. A collection eXo created is asked first; when a collection fails on its own (gone, or any error status other than 401, 403, 407 and a gateway status) the next calendar is asked, up to MAX_CAPABILITY_PROBES, while refused credentials (a 403 among them: on a read verb it cannot be told from a credential refusal) and an unreachable server end the listing at once, since asking again would only add failed requests. Every share operation asks its own collection again.

      The server's answer — the mechanism it offers, and which imported collections the caller owns — stands for exo.caldav.share.probeMemoSeconds (300 by default; 0 asks every time), since agenda asks on every refresh of its panel and every open of a calendar's menu. What is read from eXo's own storage — the account, the pairs, the calendars — is read every time, so a calendar exported or imported a moment ago appears at once; only a server reconfigured to offer sharing, or an imported collection whose ownership changed on the server, waits out the memo. A failed probe is never remembered.

      Parameters:
      userIdentityId - the caller
      username - the caller's login
      Returns:
      the agenda ids of the shareable calendars, possibly empty
    • listShares

      public org.exoplatform.caldav.model.CalendarShares listShares(long userIdentityId, String username, long calendarId) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Who a calendar of the caller's is shared with, read from the server now.
      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      Returns:
      the sharees
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - when it is bound to no collection eXo can share
      CaldavShareException - when the account, the server or its answer stops the read
    • serverShares

      public CaldavCalendarShareService.ServerShares serverShares(long userIdentityId, String username, long calendarId) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Who a calendar of the caller's is shared with, and the collection those shares live on, from one resolution of the calendar (EXO-90385).

      What agenda's channel asks when the owner opens the Share drawer. Before it existed the channel asked sharedCollectionOf(long, java.lang.String, long) and listShares(long, java.lang.String, long), each resolving the calendar, its account and its pair again; the collection is a by-product of that resolution, so answering both together costs nothing and spares the second one.

      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      Returns:
      the collection and the shares, the shares carrying the meeting-copies flag
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - when it is bound to no collection eXo can share
      CaldavShareException - when the account, the server or its answer stops the read
    • grant

      public org.exoplatform.caldav.model.CalendarShares grant(long userIdentityId, String username, long calendarId, String shareeUsername) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Gives one colleague read access to a calendar of the caller's.

      Idempotent: a colleague who can already read it — through a grant eXo made or one made elsewhere — is left as they are and nothing is written. On BlueMind that holds for plain view access only: its access list shows a colleague holding more as reading too, and such a colleague is refused (NOT_READ_ONLY) rather than rewritten, as is one holding only other access given outside eXo (SHAREE_HAS_OTHER_ACCESS).

      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      shareeUsername - the colleague's login
      Returns:
      the sharees as the server lists them after the grant
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - with a caldav.share.* code when the calendar or the sharee cannot be shared with
      CaldavShareException - when the account, the server or its answer stops the grant
    • grant

      public org.exoplatform.caldav.model.CalendarShares grant(long userIdentityId, String username, long calendarId, String shareeUsername, org.exoplatform.caldav.model.CalendarShares.ShareAccess access) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Gives one colleague access to a calendar of the caller's, at the level asked for (EXO-90378).

      Reconciling, not only granting. CalendarShares.ShareAccess.WRITE over an existing read grant widens it, CalendarShares.ShareAccess.READ over an edit grant narrows it, and either over a grant that already matches writes nothing — so agenda can call this whenever a record's level changes and get the server into that state, whatever it was in.

      The refusal ladder is unchanged in shape, only in what it calls an eXo grant: a sharee's own modifiable entry that is neither read-only nor edit-only — a right given outside eXo, which on Stalwart reads back as a bare DAV:write — is still refused (NOT_READ_ONLY) rather than rewritten, because writing it back would widen it.

      BlueMind carries both levels (EXO-90378): CS:read for CalendarShares.ShareAccess.READ, CS:read-write for CalendarShares.ShareAccess.WRITE, which its SharingProtocol stores as the verb Write. Established from BlueMind's published source — bluemind-public/bluemind, plugins/net.bluemind.dav.server/.../proto/sharing/, release/5.7 at 130d1376, byte-identical on 4.9 through master — not from a live probe. A 200 from it is not proof of anything, so the level is read back from the container's access list; a read-back that says less than was asked for is reported as the level the server holds, never as the one requested.

      Parameters:
      userIdentityId - identity identifier of the caller
      username - the caller, who must own the calendar
      calendarId - technical identifier of the calendar
      shareeUsername - the colleague
      access - the level to grant, CalendarShares.ShareAccess.READ or CalendarShares.ShareAccess.WRITE; never CalendarShares.ShareAccess.MORE, which names a grant eXo does not write
      Returns:
      the shares the collection holds once the list has been read back
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when there is no such calendar
      IllegalAccessException - when the caller does not own it
    • revoke

      public org.exoplatform.caldav.model.CalendarShares revoke(long userIdentityId, String username, long calendarId, String shareeUsername) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Takes access to a calendar of the caller's away from one colleague, whichever level eXo granted it at.

      Removes that colleague's eXo-shaped grants — a read grant, or an edit grant (EXO-90378) — and nothing else; a grant of any other shape was made outside eXo, is not eXo's to take back, and the whole request is refused rather than half applied. Idempotent: a colleague with no grant is left alone and nothing is written.

      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      shareeUsername - the colleague's login
      Returns:
      the sharees as the server lists them after the revoke
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - with a caldav.share.* code when the calendar or the sharee cannot be handled
      CaldavShareException - when the account, the server or its answer stops the revoke
    • candidates

      public List<org.exoplatform.caldav.model.CalendarShares.ShareUser> candidates(long userIdentityId, String username, long calendarId, String query) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      The colleagues a calendar of the caller's can be shared with: eXo users connected to the same server registration, with a recorded principal that is not the caller's own.

      Listed per eXo user, while a grant names a principal: two eXo users connected under one DAV login are two candidates, and granting to either gives both access — the server knows the login, not the person. listShares(long, java.lang.String, long) then names every user the grant reached (CalendarShares.CalendarSharee.users()). Whether the picker should say so before the grant, by grouping candidates per principal, is a product decision this method leaves open.

      The server is asked what the collection advertises, the capability check the listing, the grant and the revoke make, so that nobody is listed on a server where eXo offers no sharing. The colleagues themselves are read from eXo's own record of each connection; the server is asked who the caller is only when that is not recorded yet. Bounded by the storage's read of the server's connections; a colleague beyond that bound is not offered.

      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      query - text their login or full name must contain, ignoring case; blank for everyone
      Returns:
      the candidates, by full name
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - when it is bound to no collection eXo can share
      CaldavShareException - when sharing is not offered on the server, the credentials are refused, the server cannot be reached, the caller's principal cannot be named, or the calendar is an imported one the caller does not own on the server
    • holdsMeetingCopies

      public boolean holdsMeetingCopies(long userIdentityId, String username, long calendarId) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      Whether a calendar of the caller's, on their server, is where their eXo meeting copies are written (EXO-90357): what agenda's Share drawer asks before sharing it, through this add-on's channel plugin. A read, so eXo's own record of the destination answers when it may (meetingCopiesOf(org.exoplatform.caldav.service.CaldavCalendarShareService.ShareTarget, java.lang.String, boolean)), and the server is asked only when it may not.
      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      Returns:
      true when the calendar holds the meeting copies; false when it does not, or is not on the caller's server at all
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
    • sharedCollectionOf

      public CaldavCalendarShareService.SharedCollection sharedCollectionOf(long userIdentityId, String username, long calendarId) throws org.exoplatform.commons.exception.ObjectNotFoundException, IllegalAccessException
      The collection a calendar of the caller's is bound to on their server, and that server (EXO-90357): what agenda records on a share this add-on delivered, so the sharee's own listing of the server's collections can be told from it.
      Parameters:
      userIdentityId - the caller
      username - the caller's login
      calendarId - the agenda calendar
      Returns:
      the server and the collection href
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the calendar does not exist
      IllegalAccessException - when the caller does not own it
      IllegalArgumentException - with CALENDAR_NOT_ON_SERVER when the calendar is bound to no collection eXo can share
      CaldavShareException - with NOT_CONNECTED when the caller has no connected account