diff mbox series

[bug#69719] services: radicale: Use define-configuration.

Message ID 17984bb7ca32629fed475db7e782acd90e538077.1710122683.git.juli@incana.org
State New
Headers show
Series [bug#69719] services: radicale: Use define-configuration. | expand

Commit Message

Juliana Sims March 11, 2024, 2:04 a.m. UTC
Hello,

This beast of a patch ports the radicale system service to the
define-configuration framework for writing configuration files, instead of
simply accepting a handwritten file. It also includes documentation updates
(generated with the amazing generate-documentation procedure). The changelog
below should cover all changes to all exported symbols, but I'll give an
overview of the semantics of what I did and why for the benefit of reviews.

Essentially, I took each section of the Radicale configuration file[1] and made
it its own define-configuration object (henceforth "configuration"). I made each
field using these configurations a maybe type so that the serializer for the
configurations could write out the section header iff it was needed. I then made
each field of each configuration a maybe type as well so that the default
Radicale values are used unless the user overrides them. The logic here is that
users will be checking upstream documentation while configuring this service,
and we should match upstream's defaults as closely as possible. In the end, the
only non-maybe field is the Radicale package itself. This has the mildly annoying
effect that a default radicale-service-type produces an empty file on disk as
opposed to no file, but the alternative is to query each field individually in a
serialization guard which would complicate logic and therefore maintainability.
Simplifying maintainibility by providing users with automated checking for
configuration values is the primary goal here, and the separate configurations
were chosen specifically to avoid the increased cognitive overhead of
serialization guards, so this tradeoff feels worth it to me. I also modified the
radicale-activation function to query the configurations for the paths of the
directories it creates rather than hardcoding the default values.

I have tested this code fairly thoroughly, generating system vms that used at
least one field of each distinct type, including each section configuration.
I've also verified that radicale-activation works as expected and the
configuration file is correctly passed to the shepherd service. That said,
there's still a good chance I missed something because this is a lot of code.

There is one important thing to note about these changes for existing users.
Besides removing the option to pass a configuration file verbatim, the defaults
of a radicale-service-type have changed to match upstream. Previously the
defaults were not in line with upstream. This means users with
radicale-service-type and no custom configuration will see silent breakage. I
considered adding a news item to alert folks to this, but wasn't sure of the
protocol on that so I didn't. I think introducing these breakages is reasonable
because the defaults now match upstream and standard filesystem patterns -- /var
is not longer being used for static configuration files, for example.

I hope folks find this useful. I'm excited to deploy this when it's merged :)

Thanks,
Juli

[1] https://radicale.org/v3.html#configuration

* doc/guix.texi (radicale-configuration): Update documentation to reflect new
configuration, add new symbols.
* gnu/services/mail.scm (%default-radicale-config-file): Delete.
(radicale-auth-configuration, radicale-auth-configuration?)
(radicale-encoding-configuration, radicale-encoding-configuration?)
(radicale-logging-configuration, radicale-logging-configuration?)
(radicale-rights-configuration, radicale-rights-configuration?)
(radicale-server-configuration, radicale-server-configuration?)
(radicale-storage-configuration, radicale-storage-configuration?): New symbol.
(radicale-configuration, radicale-configuration?): Use define-configuration.
(radicale-activation, radicale-shepherd-service): Update for new
configuration format.
(radicale-activation): Use user-defined values for service files.
(radicale-service-type): Capitalize "Radicale" in description.
---
 doc/guix.texi         | 194 +++++++++++++++++++++-
 gnu/services/mail.scm | 368 ++++++++++++++++++++++++++++++++++++++----
 2 files changed, 520 insertions(+), 42 deletions(-)


base-commit: d084fb4b04a1cebb59959633660013fff495cd0d

Comments

Liliana Marie Prikler March 11, 2024, 5:20 a.m. UTC | #1
Hi Juliana,

Am Sonntag, dem 10.03.2024 um 22:04 -0400 schrieb Juliana Sims:
> [...]
> 
> +(define (radicale-serialize-field field-name value)
> +  ;; XXX We quote the un-gexp form here because otherwise symbol-
> literals are
> +  ;; treated as variables. We can get away with this because all of
> our other
> +  ;; field value types are primitives by the time they get here so
> are printed
> +  ;; the same whether or not they are quoted.
> +  #~(format #f "~a = ~a\n" #$(uglify-field-name field-name)
> '#$value))
You should not need to quote symbols here.

> +(define (radicale-serialize-boolean field-name value?)
> +  (radicale-serialize-field field-name (if value? "True" "False")))
> +
> +(define (radicale-serialize-comma-separated-string-list field-name
> value)
> +  (radicale-serialize-field field-name (string-join value ", ")))
> +
> +(define radicale-serialize-file-name radicale-serialize-field)
> +
> +(define radicale-serialize-non-negative-integer radicale-serialize-
> field)
> +
> +(define radicale-serialize-string radicale-serialize-field)
> +
> +(define radicale-serialize-symbol radicale-serialize-field)
> +
> +(define ((sanitize-delimited-symbols syms location field) value)
> +  (cond
> +   ((not (maybe-value-set? value))
> +    value)
> +   ((member value syms)
> +    (uglify-field-name value))
> +   (else
> +    (configuration-field-error (source-properties->location
> location)
> +                               field
> +                               value))))
> +
> +;; Section configuration types
> +
> +(define-configuration radicale-auth-configuration
> +  (type
> +   maybe-symbol
> +   "The method to verify usernames and passwords. Options are
> @code{none},
> +@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}.
> +
> +This value is tied to @code{htpasswd-filename} and @code{htpasswd-
> encryption}."
> +   (sanitizer
> +    (sanitize-delimited-symbols '(none htpasswd remote-user http-x-
> remote-user)
> +                                (current-source-location)
> +                                'type))
> +   (serializer radicale-serialize-field))
Note the mismatch between documented and stored type:  Your type is
"maybe-symbol", but you store strings.  Instead, you can a) move the
the logic to uglify the name to radicale-serialize-symbol or define a
radicale-serialize-uglified-symbol variant if you need the normal
radicale-serialize-symbol, or b) document that you actually take
strings.  Of course, you can also think of other solutions.

Hint: I'd personally prefer solution a).


Cheers
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index 858d5751bfe..096f6bcacdf 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -28252,21 +28252,201 @@  Mail Services
 server whose value should be a @code{radicale-configuration}.
 @end defvar
 
+@c %start of fragment
+
 @deftp {Data Type} radicale-configuration
-Data type representing the configuration of @command{radicale}.
+Data type representing the configuration of @command{radicale}. See the
+@uref{https://radicale.org/v3.html#configuration, configuration
+documentation} for default values.  Available
+@code{radicale-configuration} fields are:
 
 @table @asis
-@item @code{package} (default: @code{radicale})
-The package that provides @command{radicale}.
+@item @code{package} (default: @code{radicale}) (type: package)
+Package that provides @command{radicale}.
 
-@item @code{config-file} (default: @code{%default-radicale-config-file})
-File-like object of the configuration file to use, by default it will listen
-on TCP port 5232 of @code{localhost} and use the @code{htpasswd} file at
-@file{/var/lib/radicale/users} with no (@code{plain}) encryption.
+@item @code{auth} (type: maybe-auth-config)
+Configuration for auth-related variables.  This should be a
+@code{radicale-auth-configuration}.
+
+@deftp {Data Type} radicale-auth-configuration
+Data type representing the @code{auth} section of a @command{radicale}
+configuration file.  Available @code{radicale-auth-configuration} fields
+are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+The method to verify usernames and passwords.  Options are @code{none},
+@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}.
+This value is tied to @code{htpasswd-filename} and
+@code{htpasswd-encryption}.
+
+@item @code{htpasswd-filename} (type: maybe-file-name)
+Path to the htpasswd file.  Use htpasswd or similar to generate this
+file.
+
+@item @code{htpasswd-encryption} (type: maybe-symbol)
+Encryption method used in the htpasswd file.  Options are @code{plain},
+@code{bcrypt}, and @code{md5}.
+
+@item @code{delay} (type: maybe-non-negative-integer)
+Average delay after failed login attempts in seconds.
+
+@item @code{realm} (type: maybe-string)
+Message displayed in the client when a password is needed.
+
+@end table
+
+@end deftp
+
+@item @code{encoding} (type: maybe-encoding-config)
+Configuration for encoding-related variables.  This should be a
+@code{radicale-encoding-configuration}.
+
+@deftp {Data Type} radicale-encoding-configuration
+Data type representing the @code{encoding} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-encoding-configuration} fields are:
+
+@table @asis
+@item @code{request} (type: maybe-symbol)
+Encoding for responding requests.
+
+@item @code{stock} (type: maybe-symbol)
+Encoding for storing local collections.
 
 @end table
+
 @end deftp
 
+@item @code{headers-file} (type: maybe-text-config)
+Custom HTTP headers.  This should be a file-like object.
+
+@item @code{logging} (type: maybe-logging-config)
+Configuration for logging-related variables.  This should be a
+@code{radicale-logging-configuration}.
+
+@deftp {Data Type} radicale-logging-configuration
+Data type representing the @code{logging} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-logging-configuration} fields are:
+
+@table @asis
+@item @code{level} (type: maybe-symbol)
+Set the logging level.  One of @code{debug}, @code{info},
+@code{warning}, @code{error}, or @code{critical}.
+
+@item @code{mask-passwords?} (type: maybe-boolean)
+Whether to include passwords in logs.
+
+@end table
+
+@end deftp
+
+@item @code{rights} (type: maybe-rights-config)
+Configuration for rights-related variables.  This should be a
+@code{radicale-rights-configuration}.
+
+@deftp {Data Type} radicale-rights-configuration
+Data type representing the @code{rights} section of a @command{radicale}
+configuration file.  Available @code{radicale-rights-configuration}
+fields are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+Backend used to check collection access rights.  The recommended backend
+is @code{owner-only}.  If access to calendars and address books outside
+the home directory of users is granted, clients won't detect these
+collections and will not show them to the user.  Choosing any other
+method is only useful if you access calendars and address books directly
+via URL.  Options are @code{authenticate}, @code{owner-only},
+@code{owner-write}, and @code{from-file}.
+
+@item @code{file} (type: maybe-file-name)
+File for the rights backend @code{from-file}.
+
+@end table
+
+@end deftp
+
+@item @code{server} (type: maybe-server-config)
+Configuration for server-related variables.  Ignored if WSGI is used.
+This should be a @code{radicale-server-configuration}.
+
+@deftp {Data Type} radicale-server-configuration
+Data type representing the @code{server} section of a @command{radicale}
+configuration file.  Available @code{radicale-server-configuration}
+fields are:
+
+@table @asis
+@item @code{hosts} (type: maybe-comma-separated-string-list)
+List of IP addresses that the server will bind to.
+
+@item @code{max-connections} (type: maybe-non-negative-integer)
+Maximum number of parallel connections.  Set to 0 to disable the limit.
+
+@item @code{max-content-length} (type: maybe-non-negative-integer)
+Maximum size of the request body in byetes.
+
+@item @code{timeout} (type: maybe-non-negative-integer)
+Socket timeout in seconds.
+
+@item @code{ssl?} (type: maybe-boolean)
+Whether to enable transport layer encryption.
+
+@item @code{certificate} (type: maybe-file-name)
+Path of the SSL certificate.
+
+@item @code{key} (type: maybe-file-name)
+Path to the private key for SSL.  Only effective if @code{ssl?} is
+@code{#t}.
+
+@item @code{certificate-authority} (type: maybe-file-name)
+Path to CA certificate for validating client certificates.  This can be
+used to secure TCP traffic between Radicale and a reverse proxy.  If you
+want to authenticate users with client-side certificates, you also have
+to write an authentication plugin that extracts the username from the
+certificate.
+
+@end table
+
+@end deftp
+
+@item @code{storage} (type: maybe-storage-config)
+Configuration for storage-related variables.  This should be a
+@code{radicale-storage-configuration}.
+
+@deftp {Data Type} radicale-storage-configuration
+Data type representing the @code{storage} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-storage-configuration} fields are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+Backend used to store data.  Options are @code{multifilesystem} and
+@code{multifilesystem-nolock}.
+
+@item @code{filesystem-folder} (type: maybe-file-name)
+Folder for storing local collections.  Created if not present.
+
+@item @code{max-sync-token-age} (type: maybe-non-negative-integer)
+Delete sync-tokens that are older than the specified time in seconds.
+
+@item @code{hook} (type: maybe-string)
+Command run after changes to storage.
+
+@end table
+
+@end deftp
+
+@item @code{web-interface?} (type: maybe-boolean)
+Whether to use Radicale's built-in web interface.
+
+@end table
+
+@end deftp
+
+@c %end of fragment
+
 @subsubheading Rspamd Service
 @cindex email
 @cindex spam
diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm
index afe1bb60169..2499be87722 100644
--- a/gnu/services/mail.scm
+++ b/gnu/services/mail.scm
@@ -38,10 +38,12 @@  (define-module (gnu services mail)
   #:use-module (gnu packages dav)
   #:use-module (gnu packages tls)
   #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
   #:use-module (guix modules)
   #:use-module (guix records)
   #:use-module (guix packages)
   #:use-module (guix gexp)
+  #:use-module (ice-9 curried-definitions)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
@@ -79,10 +81,21 @@  (define-module (gnu services mail)
             imap4d-service-type
             %default-imap4d-config-file
 
+            radicale-auth-configuration
+            radicale-auth-configuration?
+            radicale-encoding-configuration
+            radicale-encoding-configuration?
+            radicale-logging-configuration
+            radicale-logging-configuration?
+            radicale-rights-configuration
+            radicale-rights-configuration?
+            radicale-server-configuration
+            radicale-server-configuration?
+            radicale-storage-configuration
+            radicale-storage-configuration?
             radicale-configuration
             radicale-configuration?
             radicale-service-type
-            %default-radicale-config-file
 
             rspamd-configuration
             rspamd-service-type
@@ -1929,23 +1942,255 @@  (define imap4d-service-type
 ;;; Radicale.
 ;;;
 
-(define-record-type* <radicale-configuration>
-  radicale-configuration make-radicale-configuration
-  radicale-configuration?
-  (package     radicale-configuration-package
-               (default radicale))
-  (config-file radicale-configuration-config-file
-               (default %default-radicale-config-file)))
+;; Maybe types
 
-(define %default-radicale-config-file
-  (plain-file "radicale.conf" "
-[auth]
-type = htpasswd
-htpasswd_filename = /var/lib/radicale/users
-htpasswd_encryption = plain
+(define-maybe boolean (prefix radicale-))
+(define-maybe comma-separated-string-list (prefix radicale-))
+(define-maybe file-name (prefix radicale-))
+(define-maybe non-negative-integer (prefix radicale-))
+(define-maybe string (prefix radicale-))
+(define-maybe symbol (prefix radicale-))
+(define-maybe text-config)
 
-[server]
-hosts = localhost:5232"))
+;; Serializers and sanitizers
+
+(define (radicale-serialize-field field-name value)
+  ;; XXX We quote the un-gexp form here because otherwise symbol-literals are
+  ;; treated as variables. We can get away with this because all of our other
+  ;; field value types are primitives by the time they get here so are printed
+  ;; the same whether or not they are quoted.
+  #~(format #f "~a = ~a\n" #$(uglify-field-name field-name) '#$value))
+
+(define (radicale-serialize-boolean field-name value?)
+  (radicale-serialize-field field-name (if value? "True" "False")))
+
+(define (radicale-serialize-comma-separated-string-list field-name value)
+  (radicale-serialize-field field-name (string-join value ", ")))
+
+(define radicale-serialize-file-name radicale-serialize-field)
+
+(define radicale-serialize-non-negative-integer radicale-serialize-field)
+
+(define radicale-serialize-string radicale-serialize-field)
+
+(define radicale-serialize-symbol radicale-serialize-field)
+
+(define ((sanitize-delimited-symbols syms location field) value)
+  (cond
+   ((not (maybe-value-set? value))
+    value)
+   ((member value syms)
+    (uglify-field-name value))
+   (else
+    (configuration-field-error (source-properties->location location)
+                               field
+                               value))))
+
+;; Section configuration types
+
+(define-configuration radicale-auth-configuration
+  (type
+   maybe-symbol
+   "The method to verify usernames and passwords. Options are @code{none},
+@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}.
+
+This value is tied to @code{htpasswd-filename} and @code{htpasswd-encryption}."
+   (sanitizer
+    (sanitize-delimited-symbols '(none htpasswd remote-user http-x-remote-user)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (htpasswd-filename
+   maybe-file-name
+   "Path to the htpasswd file. Use htpasswd or similar to generate this file.")
+  (htpasswd-encryption
+   maybe-symbol
+   "Encryption method used in the htpasswd file. Options are @code{plain},
+@code{bcrypt}, and @code{md5}."
+   (sanitizer
+    (sanitize-delimited-symbols '(plain bcrypt md5)
+                                (current-source-location)
+                                'htpasswd-encryption))
+   (serializer radicale-serialize-field))
+  (delay
+   maybe-non-negative-integer
+   "Average delay after failed login attempts in seconds.")
+  (realm
+   maybe-string
+   "Message displayed in the client when a password is needed.")
+  (prefix radicale-))
+
+(define-configuration radicale-encoding-configuration
+  (request
+   maybe-symbol
+   "Encoding for responding requests.")
+  (stock
+   maybe-symbol
+   "Encoding for storing local collections.")
+  (prefix radicale-))
+
+(define-configuration radicale-logging-configuration
+  (level
+   maybe-symbol
+   "Set the logging level. One of @code{debug}, @code{info}, @code{warning},
+@code{error}, or @code{critical}."
+   (sanitizer (sanitize-delimited-symbols '(debug info warning error critical)
+                                          (current-source-location)
+                                          'level))
+   (serializer radicale-serialize-field))
+  (mask-passwords?
+   maybe-boolean
+   "Whether to include passwords in logs.")
+  (prefix radicale-))
+
+(define-configuration radicale-rights-configuration
+  (type
+   maybe-symbol
+   "Backend used to check collection access rights. The recommended backend is
+@code{owner-only}. If access to calendars and address books outside the home
+directory of users is granted, clients won't detect these collections and will
+not show them to the user. Choosing any other method is only useful if you
+access calendars and address books directly via URL. Options are
+@code{authenticate}, @code{owner-only}, @code{owner-write}, and
+@code{from-file}."
+   (sanitizer
+    (sanitize-delimited-symbols '(authenticate owner-only owner-write from-file)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (file
+   maybe-file-name
+   "File for the rights backend @code{from-file}.")
+  (prefix radicale-))
+
+(define-configuration radicale-server-configuration
+  (hosts
+   maybe-comma-separated-string-list
+   "List of IP addresses that the server will bind to.")
+  (max-connections
+   maybe-non-negative-integer
+   "Maximum number of parallel connections. Set to 0 to disable the limit.")
+  (max-content-length
+   maybe-non-negative-integer
+   "Maximum size of the request body in byetes.")
+  (timeout
+   maybe-non-negative-integer
+   "Socket timeout in seconds.")
+  (ssl?
+   maybe-boolean
+   "Whether to enable transport layer encryption.")
+  (certificate
+   maybe-file-name
+   "Path of the SSL certificate.")
+  (key
+   maybe-file-name
+   "Path to the private key for SSL. Only effective if @code{ssl?} is
+@code{#t}.")
+  (certificate-authority
+   maybe-file-name
+   "Path to CA certificate for validating client certificates. This can be used
+to secure TCP traffic between Radicale and a reverse proxy. If you want to
+authenticate users with client-side certificates, you also have to write an
+authentication plugin that extracts the username from the certificate.")
+  (prefix radicale-))
+
+(define-configuration radicale-storage-configuration
+  (type
+   maybe-symbol
+   "Backend used to store data. Options are @code{multifilesystem} and
+@code{multifilesystem-nolock}."
+   (sanitizer
+    (sanitize-delimited-symbols '(multifilesystem multifilesystem-nolock)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (filesystem-folder
+   maybe-file-name
+   "Folder for storing local collections. Created if not present.")
+  (max-sync-token-age
+   maybe-non-negative-integer
+   "Delete sync-tokens that are older than the specified time in seconds.")
+  (hook
+   maybe-string
+   "Command run after changes to storage.")
+  (prefix radicale-))
+
+;; Helpers for using section configurations in the main configuration
+
+;; XXX These indirections are necessary to avoid creating semantic ambiguity
+(define auth-config? radicale-auth-configuration?)
+(define encoding-config? radicale-encoding-configuration?)
+(define logging-config? radicale-logging-configuration?)
+(define rights-config? radicale-rights-configuration?)
+(define server-config? radicale-server-configuration?)
+(define storage-config? radicale-storage-configuration?)
+
+(define-maybe auth-config)
+(define-maybe encoding-config)
+(define-maybe logging-config)
+(define-maybe rights-config)
+(define-maybe server-config)
+(define-maybe storage-config)
+
+(define ((serialize-radicale-section name fields) _ cfg)
+  #~(format #f "[~a]\n~a\n" #$name #$(serialize-configuration cfg fields)))
+
+(define serialize-auth-config
+  (serialize-radicale-section "auth" radicale-auth-configuration-fields))
+(define serialize-encoding-config
+  (serialize-radicale-section "encoding" radicale-encoding-configuration-fields))
+(define serialize-logging-config
+  (serialize-radicale-section "logging" radicale-logging-configuration-fields))
+(define serialize-rights-config
+  (serialize-radicale-section "rights" radicale-rights-configuration-fields))
+(define serialize-server-config
+  (serialize-radicale-section "server" radicale-server-configuration-fields))
+(define serialize-storage-config
+  (serialize-radicale-section "storage" radicale-storage-configuration-fields))
+
+(define (serialize-radicale-configuration cfg)
+  (mixed-text-file
+   "radicale.conf"
+   (serialize-configuration cfg radicale-configuration-fields)))
+
+(define-configuration radicale-configuration
+  ;; Only fields whose default value does not match upstream are not maybe-types
+  (package
+   (package radicale)
+   "Package that provides @command{radicale}.")
+  (auth
+   maybe-auth-config
+   "Configuration for auth-related variables. This should be a
+@code{radicale-auth-configuration}.")
+  (encoding
+   maybe-encoding-config
+   "Configuration for encoding-related variables. This should be a
+@code{radicale-encoding-configuration}.")
+  (headers-file
+   maybe-text-config
+   "Custom HTTP headers. This should be a file-like object.")
+  (logging
+   maybe-logging-config
+   "Configuration for logging-related variables. This should be a
+@code{radicale-logging-configuration}.")
+  (rights
+   maybe-rights-config
+   "Configuration for rights-related variables. This should be a
+@code{radicale-rights-configuration}.")
+  (server
+   maybe-server-config
+   "Configuration for server-related variables. Ignored if WSGI is used. This
+should be a @code{radicale-server-configuration}.")
+  (storage
+   maybe-storage-config
+   "Configuration for storage-related variables. This should be a
+@code{radicale-storage-configuration}.")
+  (web-interface?
+   maybe-boolean
+   "Whether to use Radicale's built-in web interface."
+   (serializer
+    (lambda (_ use?)
+      #~(format #f "[web]\ntype = ~a\n\n" #$(if use? "internal" "none"))))))
 
 (define %radicale-accounts
   (list (user-group
@@ -1959,43 +2204,96 @@  (define %radicale-accounts
          (home-directory "/var/empty")
          (shell (file-append shadow "/sbin/nologin")))))
 
-(define radicale-shepherd-service
-  (match-lambda
-    (($ <radicale-configuration> package config-file)
-     (list (shepherd-service
-            (provision '(radicale))
-            (documentation "Run the radicale daemon.")
-            (requirement '(networking))
-            (start #~(make-forkexec-constructor
-                      (list #$(file-append package "/bin/radicale")
-                        "-C" #$config-file)
-                      #:user "radicale"
-                      #:group "radicale"))
-            (stop #~(make-kill-destructor)))))))
+(define (radicale-shepherd-service cfg)
+  (list (shepherd-service
+         (provision '(radicale))
+         (documentation "Run the radicale daemon.")
+         (requirement '(networking))
+         (start #~(make-forkexec-constructor
+                   (list #$(file-append (radicale-configuration-package cfg)
+                                        "/bin/radicale")
+                         "-C" #$(serialize-radicale-configuration cfg))
+                   #:user "radicale"
+                   #:group "radicale"))
+         (stop #~(make-kill-destructor)))))
 
 (define radicale-activation
   (match-lambda
-    (($ <radicale-configuration> package config-file)
+    (($ <radicale-configuration> _ auth-config _ _ _ _ _ storage-config _)
+     ;; Get values for the collections directory
+     ;; See https://radicale.org/v3.html#running-as-a-service
+     (define filesystem-folder-val
+       (if (maybe-value-set? storage-config)
+           (radicale-storage-configuration-filesystem-folder storage-config)
+           storage-config))
+     (define collections-dir
+       (if (maybe-value-set? filesystem-folder-val)
+           filesystem-folder-val
+           "/var/lib/radicale/collections"))
+     (define collections-parent-dir (dirname collections-dir))
+     ;; Get values for the password file directory
+     ;; This might be the parent directory for the collections directory,
+     ;; but we shouldn't assume that.
+     (define auth-value-set? (maybe-value-set? auth-config))
+     ;; If auth's type is 'none or unset, that means there is no authentication
+     ;; and we don't need to setup files for it
+     (define auth?
+       (and auth-value-set?
+            (not (eq? (radicale-auth-configuration-type auth-config) 'none))))
+     (define password-file-val
+       (if auth-value-set?
+           (radicale-auth-configuration-htpasswd-filename auth-config)
+           auth-config))
+     (define password-file-dir
+       (if (maybe-value-set? password-file-val)
+           (dirname password-file-val)
+           "/etc/radicale"))
      (with-imported-modules '((guix build utils))
        #~(begin
            (use-modules (guix build utils))
            (let ((uid (passwd:uid (getpw "radicale")))
                  (gid (group:gid (getgr "radicale"))))
-             (mkdir-p "/var/lib/radicale/collections")
-             (chown "/var/lib/radicale" uid gid)
-             (chown "/var/lib/radicale/collections" uid gid)
-             (chmod "/var/lib/radicale" #o700)))))))
+             ;; Collections directory perms
+             (mkdir-p #$collections-dir)
+             (chown #$collections-parent-dir uid gid)
+             (chown #$collections-dir uid gid)
+             (chmod #$collections-parent-dir #o700)
+             ;; Password file perms
+             (when #$auth?
+               ;; In theory, the password file and thus this directory should already
+               ;; exist because the user has to make them by hand
+               (mkdir-p #$password-file-dir)
+               (chown #$password-file-dir uid gid)
+               (chmod #$password-file-dir #o700))))))))
 
 (define radicale-service-type
   (service-type
    (name 'radicale)
-   (description "Run radicale, a small CalDAV and CardDAV server.")
+   (description "Run Radicale, a small CalDAV and CardDAV server.")
    (extensions
     (list (service-extension shepherd-root-service-type radicale-shepherd-service)
           (service-extension account-service-type (const %radicale-accounts))
           (service-extension activation-service-type radicale-activation)))
    (default-value (radicale-configuration))))
 
+(define (generate-radicale-documentation)
+  (generate-documentation
+   `((radicale-configuration
+      ,radicale-configuration-fields
+      (auth     radicale-auth-configuration)
+      (encoding radicale-encoding-configuration)
+      (logging  radicale-logging-configuration)
+      (rights   radicale-rights-configuration)
+      (server   radicale-server-configuration)
+      (storage  radicale-storage-configuration))
+     (radicale-auth-configuration     ,radicale-auth-configuration-fields)
+     (radicale-encoding-configuration ,radicale-encoding-configuration-fields)
+     (radicale-logging-configuration  ,radicale-logging-configuration-fields)
+     (radicale-rights-configuration   ,radicale-rights-configuration-fields)
+     (radicale-server-configuration   ,radicale-server-configuration-fields)
+     (radicale-storage-configuration  ,radicale-storage-configuration-fields))
+   'radicale-configuration))
+
 ;;;
 ;;; Rspamd.
 ;;;