Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Calendar solution for internal network

Overview

As part of my efforts to de-google my life, the Calendar solution is a fully self-hosted and FLOSS project. This project uses Radicale caldav server, which is already packaged for EL7 (but I repackage a newer version). The project also uses InfCloud which is a web client for caldav, which is loosely hardcoded to use this existing radicale instance.

Additional components include the F-droid packages of davX⁵ (carddav sync utility) and ETar, a calendar client.

Prequisites

Apache httpd is installed. TLS certificates are in use, to protect the password traffic.

Building

CentOS 7 provides radicale 1.1, which is not the current version. I adapted the radicale and various python dependencies from Fedora, and also wrote the rpm spec for InfCloud. A copy of Git repository for build-radicale-el7 exists here. See build-radicale-el7/README.md for this whole process. NOTE: this includes one patch that I wrote to allow automatic login using the reverse proxy apache ldap authentication.

I established a copr to hold the rpm packages.

Installing on production

On server1, I ran a number of steps which are also documented in the [main server log][3].

Add the copr repo.

curl https://copr.fedorainfracloud.org/coprs/bgstack15/radicale-el7/repo/epel-7/bgstack15-radicale-el7-epel-7.repo | sudo tee /etc/yum.repos.d/bgstack15-radicale-el7.repo
sudo yum install radicale3 infcloud

Customize /etc/radicale/config and /etc/infcloud/config.js.

Add redirects for my http virtual host in httpd:

# Force https for these pages or apps
RewriteCond %{HTTPS} !=on
Redirect /radicale/ https://www.example.com/radicale/
RewriteCond %{HTTPS} !=on
Redirect /calendar/ https://www.example.com/calendar/

Also add useful VirtualHost contents to my apache config file ssl-common.cnf:

# 2022-05-17
#ServerName calendar.example.com
RewriteEngine On
RewriteRule ^/radicale$ /radicale/ [R,L]
<Location "/radicale/">
   ProxyPreserveHost On
   Order deny,allow
   Deny from all
   AuthType Basic
   AuthName "LDAP protected"
   AuthBasicProvider ldap
   AuthLDAPGroupAttribute member
   AuthLDAPSubGroupClass group
   # If anonymous search is disabled, provide dn and pw.
   #AuthLDAPBindDN uid=service-account,cn=users,cn=accounts,dc=ipa,dc=example,dc=com
   #AuthLDAPBindPassword mypw
   AuthLDAPGroupAttributeIsDN On
   AuthLDAPURL "ldaps://dns1.ipa.internal.com:636 dns2.ipa.internal.com:636/cn=users,cn=accounts,dc=ipa,dc=internal,dc=com?uid,memberof,gecos?sub?(objectClass=person)"
   #?sub?(objectClass=*)
   Require valid-user
   Satisfy any
   # My radical set up uses HTTP_X_REMOTE_USER as username for authentication
   RequestHeader set X_REMOTE_USER "%{AUTHENTICATE_uid}e"
   # This does not populate correctly. Probably the ldap memberOf attribute is derived and not real?
   RequestHeader set X_GROUPS "%{AUTHENTICATE_memberOf}e"
   # This populates correctly
   RequestHeader set X_GECOS "%{AUTHENTICATE_gecos}e"
   ProxyPass        http://localhost:5232/ retry=20 connectiontimeout=300 timeout=300
   ProxyPassReverse http://localhost:5232/
   RequestHeader    set X-Script-Name /radicale
</Location>

I customized the storage of calendars (aka collections) to be on the main storage area:

sudo mkdir -p /var/server1/shares/public/Support/Systems/server1/var/lib/radicale/collections
sudo mv /var/lib/radicale/collections /var/server1/shares/public/Support/Systems/server1/var/lib/radicale
sudo ln -s /var/server1/shares/public/Support/Systems/server1/var/lib/radicale/collections /var/lib/radicale/collections
sudo semanage fcontext -a -t radicale_var_lib_t '/var/server1/shares/public/Support/Systems/server1/var/lib/radicale/collections(/.*)?'

I also added all these aforementioned config files to the host-bup config file.

Start and enable radicale service.

sudo systemctl enable radicale
sudo systemctl start radicale

Make a symlink in the web root directory that points to the infcloud contents.

sudo ln -s /usr/share/infcloud/radicale_infcloud/web /var/www/html/calendar

Making good config choices

In order for InfCloud to save which calendars are enabled/visible by default, you need to turn "settingsAccount" on in config.js. This attribute causes InfCloud to store some metadata about the user choices on the caldav server (Radicale). Without this feature, the InfCloud user cannot even change which calendars are visible!

Summary of associated files

On server1, the production server:

  • /etc/radicale/config
  • /etc/infcloud/config.js symlinked to with /var/www/calendar/
  • /etc/infcloud/cache.manifest symlinked to within /var/www/html/calendar/
  • /usr/sbin/update-infcloud-cache
  • /var/www/html/calendar is a symlink to /usr/share/infcloud/radicale_infcloud/web

Operations

Some tasks that will happen over time are listed here.

Modifying InfCloud files or config

After making any changes to anything for InfCloud (the web client), you need to update the cache manifest file. It is possible that touching the file works, but a method for updating the version number in the file is with script:

sudo update-infcloud-cache

Controling collections

To add, modify, or remove personal collections (calendars, address books, and todo lists), visit https://www.example.com/radicale/. Log in with a domain credential.

Adding a client

To connect a carddav/caldav client to the Calendar service, use url https://www.example.com/radicale/ and select "Use username and password."

Using a web calendar

To use a rich web client, visit https://www.example.com/calendar/ and type in your domain username and password.

Sending invitations for event from web calendar

In the web client, select an event. Select button "Download," which saves a .ics file. Send this .ics file in your preferred mail client to your guests.

Importing event to web calendar

Feature not implemented yet. Gotta say unh!

Sharing calendar with other Radicale user

This step has to happen first, before the following Sharing operations can work.

An admin has to modify file storage:/etc/radicale/rights and add some steps. Create one rule per [uniquely-named-section]. Use uppercase permission letters for a "root" collection, i.e., a username. Use lowercase letters for any child collections. The uuid of a collection is required: the pretty name is not accepted.

The following is a way to allow all authenticated users to enumerate the items owned by domainjoin, and also read all those items.

[public-principal]
user: .+
collection: domainjoin
permissions: rRi
[public-calendars]
user: .+
collection: domainjoin/[^/]+
permissions: rRi

Adding a "w" or "W" as appropriate to the permissions: entry would enable write access. Another example, for username2 to write to a specific bgstack15 calendar:

[rule1]
user: username2
collection: bgstack15
permissions: R
[rule2]
user: username2
collection: bgstack15/68cb9dbf-7546-8023-ca4c-c69bc64918a2
permissions: rwi

It is probable that read access to the root collection is required in order for the web calendar to be able to enumerate the one shared calendar, but further experimentation should be done.

Opening a shared calendar in web calendar

Unfortunately this so far is only known to work by modifying /etc/infcloud/config.js which requires admin access to server1. If all users of the web calendar should be able to view/edit a collection, add the owning user to attribute additionalResources: ['user5'] inside var globalNetworkCheckSettings. In this example, user5 owns a shared collection, as defined by heading Sharing calendar with other Radicale user. Of course, after modifying config.js, run update-infcloud-cache.

For a specific login to access another specific login root collection (because InfCloud relies on enumerating collections underneath a root collection, i.e., user, rather than pointing to a specific collectioN), you can use the custom stackrpms attribute perUserAdditionalResources such as this example:

var globalNetworkCheckSettings={
  perUserAdditionalResources: [
     { name:"bgstack15", allowed: ["username2"] },
     { name:"username2", allowed: ["bgstack15"] }
  ]
}

Now, the shared collections should populate in the web calendar. If the logged-in user does not have access to this resource, an unfixable and unhideable exclamation mark "!" will appear in the web calendar in the left-hand side menu. It will not be clear to the user what has gone wrong.

Opening a shared calendar with davX⁵

Unfortunately the only plan for accessing shared calendars is to set up a new connection in DavX⁵ with the exact url of the target calendar, e.g. https://www.example.com/radicale/domainjoin/338119a7-3556-0a9b-67ab-be391db1ae77/ which is selectable for copy-paste when the owning user visits /radicale/ and enumerates his collections.

This task will make a new entry that enumerates that logged-in user collections, and also this one exact collection.

Future improvements

Investigate more calendar sharing. Migrate address books.

References

Internal files

[3]: server1 history log

Git repositories

[6]: https://gitlab.com/bgstack15/radicale_auth_ldap.git I ended up not using this, but it is worthy of mentioning.

Weblinks

conaclos An interesting alternative is Radicale 1. Both are very lightweight. I mainly chose Radicale over Baïkal because it is written in Python. On the client side DAVx⁵ 2 do a good job. 1 https://radicale.org/v3.html 2 https://www.davx5.com/ cube00 I've been using Radicale for a few years now and it has been fantastic. Extremely lightweight but also quite flexible with its permissions model since we have a shared family calendar. The backend storage is simply ics/vcf files and while I'm sure it's not the most efficient if you had a large number of users, for our small group it's been perfect and very satisfying knowing your data is there in plain text files. Although if I'm honest I'm just cheap and wanted to get by on the smallest VM offered by my cloud provider and NextCloud was too demanding for that. jamessb > Extremely lightweight but also quite flexible with its permissions model since we have a shared family calendar. How are you doing this? A while ago I skimmed the documentation for a couple of CalDAV servers to try and figure out how I could self-host a shared calendar, but couldn't see an easy way to do this. I've just done some more searching, and it seems there are two suggested ways to do this with Radicale: * create a separate account for the shared calendar, and tell everyone who needs write access the password * create the calendar in one use's directory, and add a symlink to it in the user directories for any other users who need write access. Both of which seem like a bit of hack compared to bring able to explicitly state that a list of users have write access to a calendar in a config file or through a UI. cube00 I created a collection with the name of our domain name and then used this example1 to regex the domain out of the user's login email address to allow them access to the shared collection. 1: https://github.com/Kozea/Radicale/blob/497b5141b066d266c318e...

Example: Grant users of the form user@domain.tld read access to the

collection "domain.tld"

Allow reading the domain collection

[read-domain-principal]

user: .+@([^@]+)

collection:

permissions: R

Allow reading all calendars and address books that are direct children of

the domain collection

[read-domain-calendars]

user: .+@([^@]+)

collection: {0}/[^/]+

permissions: r

Comments