Collection Sharing

Static collection sharing without permissions filter using soft-links (Unix-only) is supported since storage type multifilesystem was implemented, see Wiki: Sharing Collections

With 3.7.0 major extension was implemented

Sharing Implementation

Implementation of sharing collections is done by using a database to lookup the URI and in case entry exists by mapping to target URI and replacing provided data on request and adjust if required data in response.

Permissions are filtered by provided Permissions.

Sharing Configuration

New section [sharing] controls sharing configuration, see DOCUMENTATION:Sharing for details

Sharing Configuration Store

Types of supported sharing configuration:

Sharing Configuration Entry Data

Enabled*: owner AND user have to enable a share to become usable

Hidden*: owner AND user have to disable a share to become visible in PROPFIND

Supported Conversions

Sharing Configuration Entry Storage

CSV

One CSV file containing one row per sharing config, separated by ; and containing header with columns from above.

If given, properties are stored in JSON format in CSV.

Files

File-based configuration store is using encoded PathOrToken as filename for each config. File contains the data stored as "dict" in binary Python "pickle" format (same is also used for item cache files).

Sharing Request Handling

CxDAV requests

CxDav request "(DELETE|GET|HEAD|PUT)"

CxDav request "REPORT"

CxDav request "PROPFIND" without HTTP_DEPTH=1

CxDav request "PROPFIND" with HTTP_DEPTH=1

CxDav request "PROPPATCH"

CxDav request "(MKCALENDAR|MKCOL)"

CxDav request "(MOVE)"

Sharing Access

Sharing Access via Maps

Map-based sharing can be accessed as usual after authentication and authorization.

Permission Control

Workflow

In case share should be visible using PROPFIND

Sharing Access via Tokens

Token-based sharing can be accessed after retrieving the token via

Token-URI: /.token/<Token>

Note: requests to not enabled or not even defined tokens will result in 401 Not Authorized

Permission Control

Workflow

Sharing Configuration Management API version 1

Type: POST API

Base-URI: /.sharing/v1/<ShareType>/<Hook>

See also test cases in radicale/tests/test_sharing.py

Data Format

Input Data Format

Parsing be controlled by CONTENT_TYPE

Output Data Format

Can be selected by HTTP_ACCEPT - default is equal to provided CONTENT_TYPE

Accepted Input Data Fields

API Hooks

API Hook "info"

Shows what is active/supported like ShareTypes(Feature), Conversions or permission to create or use properties overlay (depending on config options)

form->text
curl -u user:$userpw -H "accept: text/plain" -d "" http://localhost:5232/.sharing/v1/all/info
ApiVersion=1
Status='success'
FeatureEnabledCollectionByMap=True
PermittedCreateCollectionByMap=True
FeatureEnabledCollectionByToken=True
PermittedCreateCollectionByToken=True
SupportedConversions=(bday none)
PermittedPropertiesOverlay=True
SupportedPropertiesOverlay=(C:calendar-description ICAL:calendar-color CR:addressbook-description INF:addressbook-color D:displayname)
json->json, parsed with jq
bash
curl -u user:$userpw --silent -H "accept: application/json" -d "" http://localhost:5232/.sharing/v1/all/info | jq
{
  "ApiVersion": 1,
  "Status": "success",
  "FeatureEnabledCollectionByMap": true,
  "PermittedCreateCollectionByMap": true,
  "FeatureEnabledCollectionByToken": true,
  "PermittedCreateCollectionByToken": true,
  "SupportedConversions": ["bday", "none"],
  "PermittedPropertiesOverlay": true,
  "SupportedPropertiesOverlay": ["C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname"]
}

API Hook "(token|map)/create"

API Hook "token/create"

Create a share by mapping a collection of an Owner to a token.

| Parameter | Type | Requirement | | - | - | - | | PathMapped | str | mandatory | | Conversion | str | optional (default:none) | | User | str | optional (default:owner) | | Permissions | str | optional (default:rp) | | Enabled | bool | optional (owner/default:False) | | Hidden | bool | optional (owner/default:True) | | Properties | str | optional |

| Parameter | Type | Value | | - | - | - | | PathOrToken | str | (autogenerated token) |

form->text
curl -u user:$userpw -d "PathMapped=/user/testcalendar1/" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/token/create
ApiVersion=1
Status='success'
PathOrToken='/.token/v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw0/'
json->json
curl -u user:$userpw -H "Content-Type: application/json" -d '{ "PathMapped": "/user/testcalendar1/", "Enabled": true, "Hidden": false}' http://localhost:5232/.sharing/v1/token/create
{"ApiVersion": 1, "Status": "success", "PathOrToken": "/.token/v1/aMsmGqOsRwSH-2-6tEa8EMr4RMYzMU7WvPmjnp5qDnw0/"}
API Hook "map/create"

Create a share by mapping a collection of an Owner to an User.

| Parameter | Type | Requirement | | - | - | - | | PathOrToken | str | mandatory | | PathMapped | str | mandatory | | Conversion | str | optional (default:none) | | User | str | mandatory | | Permissions | str | optional (default:r) | | Enabled | bool | optional (owner/default:False) | | Hidden | bool | optional (owner/default:True) | | Properties | optional |

form->text
curl -u owner:$ownerpw -d "PathOrToken=/user/cal1-from-owner/" -d "PathMapped=/owner/testcalendar1/" -d "User=user" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/map/create
ApiVersion=1
Status='success'
json->json
curl -u owner:$ownerpw -H "Content-Type: application/json" -d '{ "PathOrToken": "/user/cal1-from-owner/", "PathMapped": "/owner/testcalendar1/", "User" : "user", "Enabled": true, "Hidden": false}' http://localhost:5232/.sharing/v1/map/create
{"ApiVersion": 1, "Status": "success"}

API Hook "(all|token|map)/list"

List shares (optional with filter) either owned or assigned as user.

| Parameter | Type | Used for | | - | - | - | | PathOrToken | str | optional | | PathMapped | str | optional |

form->text ("all")
curl -u user:$userpw -d "" http://localhost:5232/.sharing/v1/all/list
ApiVersion=1
Lines=1
Status='success'
Fields="ShareType;PathOrToken;PathMapped;Owner;User;Permissions;EnabledByOwner;EnabledByUser;HiddenByOwner;HiddenByUser;TimestampCreated;TimestampUpdated;Properties"
Content[0]="map;/user/cal1-from-owner/;/owner/testcalendar1/;owner;user;r;True;True;False;False;1772748001;1772748163;
form->csv ("map" only)
curl -H "accept: text/csv" -u user:$userpw -d "" http://localhost:5232/.sharing/v1/map/list
ShareType;PathOrToken;PathMapped;Owner;User;Permissions;EnabledByOwner;EnabledByUser;HiddenByOwner;HiddenByUser;TimestampCreated;TimestampUpdated;Properties
map;/user/cal1-from-owner/;/owner/testcalendar1/;owner;user;r;True;False;False;True;1772747277;1772747277;
json->json ("all"), parsed with jq
curl -s -H "Content-Type: application/json" -u user:$userpw -d "{}" http://localhost:5232/.sharing/v1/all/list | jq
{
  "ApiVersion": 1,
  "Lines": 2,
  "Status": "success",
  "Content": [
    {
      "ShareType": "map",
      "PathOrToken": "/user/cal1-from-owner/",
      "PathMapped": "/owner/testcalendar1/",
      "Owner": "owner",
      "User": "user",
      "Permissions": "r",
      "EnabledByOwner": true,
      "EnabledByUser": false,
      "HiddenByOwner": false,
      "HiddenByUser": true,
      "TimestampCreated": 1772747277,
      "TimestampUpdated": 1772747277,
      "Properties": ""
    },
    {
      "ShareType": "token",
      "PathOrToken": "v1/DUSl_J5rRlWx3fy8YRXpH22FFllplkOTpcSwfGtpvkc=",
      "PathMapped": "/user/testcalendar1/",
      "Owner": "user",
      "User": "user",
      "Permissions": "r",
      "EnabledByOwner": true,
      "EnabledByUser": false,
      "HiddenByOwner": false,
      "HiddenByUser": true,
      "TimestampCreated": 1772747371,
      "TimestampUpdated": 1772747371,
      "Properties": ""
    }
  7]
}

API Hook "(token|map)/delete"

Delete a share selected by PathOrToken.

| Parameter | Type | Used for | as Owner | as User | | - | - | - | - | - | | PathOrToken | str | selection | mandatory | not-permitted |

form->text
curl -u owner:$ownerpw -d "PathOrToken=/user/cal1-from-owner/" http://localhost:5232/.sharing/v1/map/delete
ApiVersion=1
Status='success'
json->json
curl -u user:$userpw -H "Content-Type: application/json" -d '{ "PathOrToken": "v1/DUSl_J5rRlWx3fy8YRXpH22FFllplkOTpcSwfGtpvkc="}' http://localhost:5232/.sharing/v1/token/delete
{"ApiVersion": 1, "Status": "success"}

API Hook "(token|map)/update"

Update a share selected by PathOrToken.

Execute delete+create in case PathOrToken needs to be changed.

| Parameter | Type | Used for | Owner | User | | - | - | - | - | - | | PathOrToken | str | selection | mandatory | mandatory | | PathMapped | str | adjust | optional | not-permitted | | User | str | adjust | optional | not-permitted | | Permissions | str | adjust | optional | not-permitted | | Enabled | bool | adjust | optional(owner) | optional(user) | | Hidden | bool | adjust | optional(owner) | optional(user) | | Properties | str | adjust | optional | optional |

form->text
curl -u user:$userpw -d "PathOrToken=/user/cal1-from-owner/" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/map/update
ApiVersion=1
Status='success'
json->json
curl -u user:$userpw -H "Content-Type: application/json" -d '{ "PathOrToken": "/user/cal1-from-owner/", "Enabled": true, "Hidden": false}' http://localhost:5232/.sharing/v1/map/update
{"ApiVersion": 1, "Status": "success"}

API Hooks "(token|map)/(enable|disable|hide|unhide)"

Toggle enable|disable|hide|unhide of Owner or User of a share selected by PathOrToken

| Parameter | Type | Used for | Owner | User | | - | - | - | - | - | | PathOrToken | selection | mandatory | mandatory |

form->text
curl -u user:$userpw -d "PathOrToken=/user/cal1-from-owner/" http://localhost:5232/.sharing/v1/map/enable
ApiVersion=1
Status='success'
json->json
curl -u user:$userpw -H "Content-Type: application/json" -d '{ "PathOrToken": "/user/cal1-from-owner/"}' http://localhost:5232/.sharing/v1/map/unhide
{"ApiVersion": 1, "Status": "success"}

Properties Overlay

Owner or user can define per share a set of properties to overlay on PROPFIND response during create or update via API.

Whitelisted ones are defined in OVERLAY_PROPERTIES_WHITELIST in radicale/sharing/__init__.py:

Properties Overlay Control Options

Properties Overlay Control Precedence

General Permission for Overlay
  1. permission of particular share configuration: p or P

  2. permission based on rights per location: p or P

  3. config option: permit_properties_overlay

Enforce Overlay on Write
  1. permission of particular share configuration: e or E

  2. permission based on rights per location: e or E

  3. config option: enforce_properties_overlay

Properties Overlay Example

Requirements

Test sequence

## PROPFIND color
xml_pfc='<?xml version="1.0"?>
<propfind xmlns="DAV:" xmlns:ICAL="http://apple.com/ns/ical/">
  <prop>
    <ICAL:calendar-color />
  </prop>
</propfind>'

## PROPPATCH color
xml_ppc='<?xml version="1.0"?>
<D:propertyupdate xmlns:D="DAV:">
  <D:set>
    <D:prop>
      <I:calendar-color xmlns:I="http://apple.com/ns/ical/">#DDDDDD</I:calendar-color>
    </D:prop>
  </D:set>
</D:propertyupdate>'
## Retrieve collection color of owner (no color set)
curl -u owner:$ownerpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/owner/testcalendar1/

## Create read-only share for user
curl -u owner:$ownerpw -d "PathOrToken=/user/cal1-from-owner/" -d "PathMapped=/owner/testcalendar1/" -d "User=user" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/map/create

## Accept (enable+unhide) share by user
curl -u user:$userpw -d "PathOrToken=/user/cal1-from-owner/" -d "Enabled=True" -d "Hidden=False" http://localhost:5232/.sharing/v1/map/update

## Retrieve collection color of share by user (no color set)
curl -u user:$userpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/user/cal1-from-owner/

## Set property overlay by user
curl -u user:$userpw -d "PathOrToken=/user/cal1-from-owner/" -d 'Properties="ICAL:calendar-color"="#CCCCCC"' http://localhost:5232/.sharing/v1/map/update

## Retrieve collection color of share by user (color set)
curl -u user:$userpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/user/cal1-from-owner/

## Delete property overlay by user
curl -u user:$userpw -d "PathOrToken=/user/cal1-from-owner/" -d 'Properties=' http://localhost:5232/.sharing/v1/map/update

## Retrieve collection color of share by user (no color set)
url -u user:$userpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/user/cal1-from-owner/

## Add property overlay by user using PROPPATCH
curl -u user:$userpw -d "$xml_ppc" -X PROPPATCH http://localhost:5232/user/cal1-from-owner/

## Retrieve collection color of share by user (color set)
curl -u user:$userpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/user/cal1-from-owner/

Virtual "bday" collection

Owner can create for itself or for particular user a virtual bday collection from an existing addressbook.

Examples

Preconditions:

Examples using API

## Create sharing of type *map* with conversion *bday*
curl -u owner:$ownerpw -d "PathOrToken=/owner/bday-of-addressbook/" -d "PathMapped=/owner/addressbook/" -d "User=owner" -d "Conversion=bday" http://localhost:5232/.sharing/v1/map/create

## Enable
curl -u owner:$ownerpw -d "PathOrToken=/owner/bday-of-addressbook/" http://localhost:5232/.sharing/v1/map/enable

## Unhide
curl -u owner:$ownerpw -d "PathOrToken=/owner/bday-of-addressbook/" http://localhost:5232/.sharing/v1/map/unhide
## Fetch VCALENDAR auto-created from VADDRESSBOOK
curl -u owner:$ownerpw http://localhost:5232/owner/bday-of-addressbook/
BEGIN:VCALENDAR
...
END:VCALENDAR

Via WebUI an additional (virtual) calendar collection appears

## Create sharing of type *token* with conversion *bday* (shown PathOrToken is an example)
curl -u owner:$ownerpw -d "Enabled=true" -d "Hidden=false" -d "PathMapped=/owner/addressbook/" -d "User=owner" -d "Conversion=bday" http://localhost:5232/.sharing/v1/token/create
ApiVersion=1
Status='success'
PathOrToken='/.token/v1/lqqwqhZYTGi9uSPsixien_8G5jiSK0FfhNFRGG_t8UA0/'
## Fetch VCALENDAR auto-created from VADDRESSBOOK
curl http://localhost:5232/.token/v1/lqqwqhZYTGi9uSPsixien_8G5jiSK0FfhNFRGG_t8UA0/
BEGIN:VCALENDAR
...
END:VCALENDAR