1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.radicale;
7
8 format = pkgs.formats.ini {
9 listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
10 };
11
12 pkg = if isNull cfg.package then
13 pkgs.radicale
14 else
15 cfg.package;
16
17 confFile = if cfg.settings == { } then
18 pkgs.writeText "radicale.conf" cfg.config
19 else
20 format.generate "radicale.conf" cfg.settings;
21
22 rightsFile = format.generate "radicale.rights" cfg.rights;
23
24 bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
25
26in {
27 options.services.radicale = {
28 enable = mkEnableOption "Radicale CalDAV and CardDAV server";
29
30 package = mkOption {
31 description = "Radicale package to use.";
32 # Default cannot be pkgs.radicale because non-null values suppress
33 # warnings about incompatible configuration and storage formats.
34 type = with types; nullOr package // { inherit (package) description; };
35 default = null;
36 defaultText = "pkgs.radicale";
37 };
38
39 config = mkOption {
40 type = types.str;
41 default = "";
42 description = ''
43 Radicale configuration, this will set the service
44 configuration file.
45 This option is mutually exclusive with <option>settings</option>.
46 This option is deprecated. Use <option>settings</option> instead.
47 '';
48 };
49
50 settings = mkOption {
51 type = format.type;
52 default = { };
53 description = ''
54 Configuration for Radicale. See
55 <link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
56 This option is mutually exclusive with <option>config</option>.
57 '';
58 example = literalExample ''
59 server = {
60 hosts = [ "0.0.0.0:5232" "[::]:5232" ];
61 };
62 auth = {
63 type = "htpasswd";
64 htpasswd_filename = "/etc/radicale/users";
65 htpasswd_encryption = "bcrypt";
66 };
67 storage = {
68 filesystem_folder = "/var/lib/radicale/collections";
69 };
70 '';
71 };
72
73 rights = mkOption {
74 type = format.type;
75 description = ''
76 Configuration for Radicale's rights file. See
77 <link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
78 This option only works in conjunction with <option>settings</option>.
79 Setting this will also set <option>settings.rights.type</option> and
80 <option>settings.rights.file</option> to approriate values.
81 '';
82 default = { };
83 example = literalExample ''
84 root = {
85 user = ".+";
86 collection = "";
87 permissions = "R";
88 };
89 principal = {
90 user = ".+";
91 collection = "{user}";
92 permissions = "RW";
93 };
94 calendars = {
95 user = ".+";
96 collection = "{user}/[^/]+";
97 permissions = "rw";
98 };
99 '';
100 };
101
102 extraArgs = mkOption {
103 type = types.listOf types.str;
104 default = [];
105 description = "Extra arguments passed to the Radicale daemon.";
106 };
107 };
108
109 config = mkIf cfg.enable {
110 assertions = [
111 {
112 assertion = cfg.settings == { } || cfg.config == "";
113 message = ''
114 The options services.radicale.config and services.radicale.settings
115 are mutually exclusive.
116 '';
117 }
118 ];
119
120 warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
121 The configuration and storage formats of your existing Radicale
122 installation might be incompatible with the newest version.
123 For upgrade instructions see
124 https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
125 Set services.radicale.package to suppress this warning.
126 '' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
127 The configuration format of your existing Radicale installation might be
128 incompatible with the newest version. For upgrade instructions see
129 https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
130 Set services.radicale.package to suppress this warning.
131 '' ++ optional (cfg.config != "") ''
132 The option services.radicale.config is deprecated.
133 Use services.radicale.settings instead.
134 '';
135
136 services.radicale.settings.rights = mkIf (cfg.rights != { }) {
137 type = "from_file";
138 file = toString rightsFile;
139 };
140
141 environment.systemPackages = [ pkg ];
142
143 users.users.radicale.uid = config.ids.uids.radicale;
144
145 users.groups.radicale.gid = config.ids.gids.radicale;
146
147 systemd.services.radicale = {
148 description = "A Simple Calendar and Contact Server";
149 after = [ "network.target" ];
150 requires = [ "network.target" ];
151 wantedBy = [ "multi-user.target" ];
152 serviceConfig = {
153 ExecStart = concatStringsSep " " ([
154 "${pkg}/bin/radicale" "-C" confFile
155 ] ++ (
156 map escapeShellArg cfg.extraArgs
157 ));
158 User = "radicale";
159 Group = "radicale";
160 StateDirectory = "radicale/collections";
161 StateDirectoryMode = "0750";
162 # Hardening
163 CapabilityBoundingSet = [ "" ];
164 DeviceAllow = [ "/dev/stdin" ];
165 DevicePolicy = "strict";
166 IPAddressAllow = mkIf bindLocalhost "localhost";
167 IPAddressDeny = mkIf bindLocalhost "any";
168 LockPersonality = true;
169 MemoryDenyWriteExecute = true;
170 NoNewPrivileges = true;
171 PrivateDevices = true;
172 PrivateTmp = true;
173 PrivateUsers = true;
174 ProcSubset = "pid";
175 ProtectClock = true;
176 ProtectControlGroups = true;
177 ProtectHome = true;
178 ProtectHostname = true;
179 ProtectKernelLogs = true;
180 ProtectKernelModules = true;
181 ProtectKernelTunables = true;
182 ProtectProc = "invisible";
183 ProtectSystem = "strict";
184 ReadWritePaths = lib.optional
185 (hasAttrByPath [ "storage" "filesystem_folder" ] cfg.settings)
186 cfg.settings.storage.filesystem_folder;
187 RemoveIPC = true;
188 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
189 RestrictNamespaces = true;
190 RestrictRealtime = true;
191 RestrictSUIDSGID = true;
192 SystemCallArchitectures = "native";
193 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
194 UMask = "0027";
195 };
196 };
197 };
198
199 meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
200}