1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.taskserver;
7
8 taskd = "${pkgs.taskserver}/bin/taskd";
9
10 mkVal = val:
11 if val == true then "true"
12 else if val == false then "false"
13 else if isList val then concatStringsSep ", " val
14 else toString val;
15
16 mkConfLine = key: val: let
17 result = "${key} = ${mkVal val}";
18 in optionalString (val != null && val != []) result;
19
20 mkManualPkiOption = desc: mkOption {
21 type = types.nullOr types.path;
22 default = null;
23 description = desc + ''
24 <note><para>
25 Setting this option will prevent automatic CA creation and handling.
26 </para></note>
27 '';
28 };
29
30 manualPkiOptions = {
31 ca.cert = mkManualPkiOption ''
32 Fully qualified path to the CA certificate.
33 '';
34
35 server.cert = mkManualPkiOption ''
36 Fully qualified path to the server certificate.
37 '';
38
39 server.crl = mkManualPkiOption ''
40 Fully qualified path to the server certificate revocation list.
41 '';
42
43 server.key = mkManualPkiOption ''
44 Fully qualified path to the server key.
45 '';
46 };
47
48 mkAutoDesc = preamble: ''
49 ${preamble}
50
51 <note><para>
52 This option is for the automatically handled CA and will be ignored if any
53 of the <option>services.taskserver.pki.manual.*</option> options are set.
54 </para></note>
55 '';
56
57 mkExpireOption = desc: mkOption {
58 type = types.nullOr types.int;
59 default = null;
60 example = 365;
61 apply = val: if isNull val then -1 else val;
62 description = mkAutoDesc ''
63 The expiration time of ${desc} in days or <literal>null</literal> for no
64 expiration time.
65 '';
66 };
67
68 autoPkiOptions = {
69 bits = mkOption {
70 type = types.int;
71 default = 4096;
72 example = 2048;
73 description = mkAutoDesc "The bit size for generated keys.";
74 };
75
76 expiration = {
77 ca = mkExpireOption "the CA certificate";
78 server = mkExpireOption "the server certificate";
79 client = mkExpireOption "client certificates";
80 crl = mkExpireOption "the certificate revocation list (CRL)";
81 };
82 };
83
84 needToCreateCA = let
85 notFound = path: let
86 dotted = concatStringsSep "." path;
87 in throw "Can't find option definitions for path `${dotted}'.";
88 findPkiDefinitions = path: attrs: let
89 mkSublist = key: val: let
90 newPath = path ++ singleton key;
91 in if isOption val
92 then attrByPath newPath (notFound newPath) cfg.pki.manual
93 else findPkiDefinitions newPath val;
94 in flatten (mapAttrsToList mkSublist attrs);
95 in all isNull (findPkiDefinitions [] manualPkiOptions);
96
97 configFile = pkgs.writeText "taskdrc" (''
98 # systemd related
99 daemon = false
100 log = -
101
102 # logging
103 ${mkConfLine "debug" cfg.debug}
104 ${mkConfLine "ip.log" cfg.ipLog}
105
106 # general
107 ${mkConfLine "ciphers" cfg.ciphers}
108 ${mkConfLine "confirmation" cfg.confirmation}
109 ${mkConfLine "extensions" cfg.extensions}
110 ${mkConfLine "queue.size" cfg.queueSize}
111 ${mkConfLine "request.limit" cfg.requestLimit}
112
113 # client
114 ${mkConfLine "client.allow" cfg.allowedClientIDs}
115 ${mkConfLine "client.deny" cfg.disallowedClientIDs}
116
117 # server
118 server = ${cfg.listenHost}:${toString cfg.listenPort}
119 ${mkConfLine "trust" cfg.trust}
120
121 # PKI options
122 ${if needToCreateCA then ''
123 ca.cert = ${cfg.dataDir}/keys/ca.cert
124 server.cert = ${cfg.dataDir}/keys/server.cert
125 server.key = ${cfg.dataDir}/keys/server.key
126 server.crl = ${cfg.dataDir}/keys/server.crl
127 '' else ''
128 ca.cert = ${cfg.pki.ca.cert}
129 server.cert = ${cfg.pki.server.cert}
130 server.key = ${cfg.pki.server.key}
131 server.crl = ${cfg.pki.server.crl}
132 ''}
133 '' + cfg.extraConfig);
134
135 orgOptions = { name, ... }: {
136 options.users = mkOption {
137 type = types.uniq (types.listOf types.str);
138 default = [];
139 example = [ "alice" "bob" ];
140 description = ''
141 A list of user names that belong to the organization.
142 '';
143 };
144
145 options.groups = mkOption {
146 type = types.listOf types.str;
147 default = [];
148 example = [ "workers" "slackers" ];
149 description = ''
150 A list of group names that belong to the organization.
151 '';
152 };
153 };
154
155 certtool = "${pkgs.gnutls.bin}/bin/certtool";
156
157 nixos-taskserver = pkgs.buildPythonPackage {
158 name = "nixos-taskserver";
159 namePrefix = "";
160
161 src = pkgs.runCommand "nixos-taskserver-src" {} ''
162 mkdir -p "$out"
163 cat "${pkgs.substituteAll {
164 src = ./helper-tool.py;
165 inherit taskd certtool;
166 inherit (cfg) dataDir user group fqdn;
167 certBits = cfg.pki.auto.bits;
168 clientExpiration = cfg.pki.auto.expiration.client;
169 crlExpiration = cfg.pki.auto.expiration.crl;
170 }}" > "$out/main.py"
171 cat > "$out/setup.py" <<EOF
172 from setuptools import setup
173 setup(name="nixos-taskserver",
174 py_modules=["main"],
175 install_requires=["Click"],
176 entry_points="[console_scripts]\\nnixos-taskserver=main:cli")
177 EOF
178 '';
179
180 propagatedBuildInputs = [ pkgs.pythonPackages.click ];
181 };
182
183in {
184 options = {
185 services.taskserver = {
186 enable = mkOption {
187 type = types.bool;
188 default = false;
189 example = true;
190 description = ''
191 Whether to enable the Taskwarrior server.
192
193 More instructions about NixOS in conjuction with Taskserver can be
194 found in the NixOS manual at
195 <olink targetdoc="manual" targetptr="module-taskserver"/>.
196 '';
197 };
198
199 user = mkOption {
200 type = types.str;
201 default = "taskd";
202 description = "User for Taskserver.";
203 };
204
205 group = mkOption {
206 type = types.str;
207 default = "taskd";
208 description = "Group for Taskserver.";
209 };
210
211 dataDir = mkOption {
212 type = types.path;
213 default = "/var/lib/taskserver";
214 description = "Data directory for Taskserver.";
215 };
216
217 ciphers = mkOption {
218 type = types.nullOr (types.separatedString ":");
219 default = null;
220 example = "NORMAL:-VERS-SSL3.0";
221 description = let
222 url = "https://gnutls.org/manual/html_node/Priority-Strings.html";
223 in ''
224 List of GnuTLS ciphers to use. See the GnuTLS documentation about
225 priority strings at <link xlink:href="${url}"/> for full details.
226 '';
227 };
228
229 organisations = mkOption {
230 type = types.attrsOf (types.submodule orgOptions);
231 default = {};
232 example.myShinyOrganisation.users = [ "alice" "bob" ];
233 example.myShinyOrganisation.groups = [ "staff" "outsiders" ];
234 example.yetAnotherOrganisation.users = [ "foo" "bar" ];
235 description = ''
236 An attribute set where the keys name the organisation and the values
237 are a set of lists of <option>users</option> and
238 <option>groups</option>.
239 '';
240 };
241
242 confirmation = mkOption {
243 type = types.bool;
244 default = true;
245 description = ''
246 Determines whether certain commands are confirmed.
247 '';
248 };
249
250 debug = mkOption {
251 type = types.bool;
252 default = false;
253 description = ''
254 Logs debugging information.
255 '';
256 };
257
258 extensions = mkOption {
259 type = types.nullOr types.path;
260 default = null;
261 description = ''
262 Fully qualified path of the Taskserver extension scripts.
263 Currently there are none.
264 '';
265 };
266
267 ipLog = mkOption {
268 type = types.bool;
269 default = false;
270 description = ''
271 Logs the IP addresses of incoming requests.
272 '';
273 };
274
275 queueSize = mkOption {
276 type = types.int;
277 default = 10;
278 description = ''
279 Size of the connection backlog, see <citerefentry>
280 <refentrytitle>listen</refentrytitle>
281 <manvolnum>2</manvolnum>
282 </citerefentry>.
283 '';
284 };
285
286 requestLimit = mkOption {
287 type = types.int;
288 default = 1048576;
289 description = ''
290 Size limit of incoming requests, in bytes.
291 '';
292 };
293
294 allowedClientIDs = mkOption {
295 type = with types; loeOf (either (enum ["all" "none"]) str);
296 default = [];
297 example = [ "[Tt]ask [2-9]+" ];
298 description = ''
299 A list of regular expressions that are matched against the reported
300 client id (such as <literal>task 2.3.0</literal>).
301
302 The values <literal>all</literal> or <literal>none</literal> have
303 special meaning. Overidden by any entry in the option
304 <option>services.taskserver.disallowedClientIDs</option>.
305 '';
306 };
307
308 disallowedClientIDs = mkOption {
309 type = with types; loeOf (either (enum ["all" "none"]) str);
310 default = [];
311 example = [ "[Tt]ask [2-9]+" ];
312 description = ''
313 A list of regular expressions that are matched against the reported
314 client id (such as <literal>task 2.3.0</literal>).
315
316 The values <literal>all</literal> or <literal>none</literal> have
317 special meaning. Any entry here overrides those in
318 <option>services.taskserver.allowedClientIDs</option>.
319 '';
320 };
321
322 listenHost = mkOption {
323 type = types.str;
324 default = "localhost";
325 example = "::";
326 description = ''
327 The address (IPv4, IPv6 or DNS) to listen on.
328
329 If the value is something else than <literal>localhost</literal> the
330 port defined by <option>listenPort</option> is automatically added to
331 <option>networking.firewall.allowedTCPPorts</option>.
332 '';
333 };
334
335 listenPort = mkOption {
336 type = types.int;
337 default = 53589;
338 description = ''
339 Port number of the Taskserver.
340 '';
341 };
342
343 fqdn = mkOption {
344 type = types.str;
345 default = "localhost";
346 description = ''
347 The fully qualified domain name of this server, which is also used
348 as the common name in the certificates.
349 '';
350 };
351
352 trust = mkOption {
353 type = types.enum [ "allow all" "strict" ];
354 default = "strict";
355 description = ''
356 Determines how client certificates are validated.
357
358 The value <literal>allow all</literal> performs no client
359 certificate validation. This is not recommended. The value
360 <literal>strict</literal> causes the client certificate to be
361 validated against a CA.
362 '';
363 };
364
365 pki.manual = manualPkiOptions;
366 pki.auto = autoPkiOptions;
367
368 extraConfig = mkOption {
369 type = types.lines;
370 default = "";
371 example = "client.cert = /tmp/debugging.cert";
372 description = ''
373 Extra lines to append to the taskdrc configuration file.
374 '';
375 };
376 };
377 };
378
379 config = mkMerge [
380 (mkIf cfg.enable {
381 environment.systemPackages = [ pkgs.taskserver nixos-taskserver ];
382
383 users.users = optional (cfg.user == "taskd") {
384 name = "taskd";
385 uid = config.ids.uids.taskd;
386 description = "Taskserver user";
387 group = cfg.group;
388 };
389
390 users.groups = optional (cfg.group == "taskd") {
391 name = "taskd";
392 gid = config.ids.gids.taskd;
393 };
394
395 systemd.services.taskserver-init = {
396 wantedBy = [ "taskserver.service" ];
397 before = [ "taskserver.service" ];
398 description = "Initialize Taskserver Data Directory";
399
400 preStart = ''
401 mkdir -m 0770 -p "${cfg.dataDir}"
402 chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
403 '';
404
405 script = ''
406 ${taskd} init
407 echo "include ${configFile}" > "${cfg.dataDir}/config"
408 touch "${cfg.dataDir}/.is_initialized"
409 '';
410
411 environment.TASKDDATA = cfg.dataDir;
412
413 unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized";
414
415 serviceConfig.Type = "oneshot";
416 serviceConfig.User = cfg.user;
417 serviceConfig.Group = cfg.group;
418 serviceConfig.PermissionsStartOnly = true;
419 serviceConfig.PrivateNetwork = true;
420 serviceConfig.PrivateDevices = true;
421 serviceConfig.PrivateTmp = true;
422 };
423
424 systemd.services.taskserver = {
425 description = "Taskwarrior Server";
426
427 wantedBy = [ "multi-user.target" ];
428 after = [ "network.target" ];
429
430 environment.TASKDDATA = cfg.dataDir;
431
432 preStart = let
433 jsonOrgs = builtins.toJSON cfg.organisations;
434 jsonFile = pkgs.writeText "orgs.json" jsonOrgs;
435 helperTool = "${nixos-taskserver}/bin/nixos-taskserver";
436 in "${helperTool} process-json '${jsonFile}'";
437
438 serviceConfig = {
439 ExecStart = "@${taskd} taskd server";
440 ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
441 Restart = "on-failure";
442 PermissionsStartOnly = true;
443 PrivateTmp = true;
444 PrivateDevices = true;
445 User = cfg.user;
446 Group = cfg.group;
447 };
448 };
449 })
450 (mkIf (cfg.enable && needToCreateCA) {
451 systemd.services.taskserver-ca = {
452 wantedBy = [ "taskserver.service" ];
453 after = [ "taskserver-init.service" ];
454 before = [ "taskserver.service" ];
455 description = "Initialize CA for TaskServer";
456 serviceConfig.Type = "oneshot";
457 serviceConfig.UMask = "0077";
458 serviceConfig.PrivateNetwork = true;
459 serviceConfig.PrivateTmp = true;
460
461 script = ''
462 silent_certtool() {
463 if ! output="$("${certtool}" "$@" 2>&1)"; then
464 echo "GNUTLS certtool invocation failed with output:" >&2
465 echo "$output" >&2
466 fi
467 }
468
469 mkdir -m 0700 -p "${cfg.dataDir}/keys"
470 chown root:root "${cfg.dataDir}/keys"
471
472 if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then
473 silent_certtool -p \
474 --bits ${toString cfg.pki.auto.bits} \
475 --outfile "${cfg.dataDir}/keys/ca.key"
476 silent_certtool -s \
477 --template "${pkgs.writeText "taskserver-ca.template" ''
478 cn = ${cfg.fqdn}
479 expiration_days = ${toString cfg.pki.auto.expiration.ca}
480 cert_signing_key
481 ca
482 ''}" \
483 --load-privkey "${cfg.dataDir}/keys/ca.key" \
484 --outfile "${cfg.dataDir}/keys/ca.cert"
485
486 chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert"
487 chmod g+r "${cfg.dataDir}/keys/ca.cert"
488 fi
489
490 if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then
491 silent_certtool -p \
492 --bits ${toString cfg.pki.auto.bits} \
493 --outfile "${cfg.dataDir}/keys/server.key"
494
495 silent_certtool -c \
496 --template "${pkgs.writeText "taskserver-cert.template" ''
497 cn = ${cfg.fqdn}
498 expiration_days = ${toString cfg.pki.auto.expiration.server}
499 tls_www_server
500 encryption_key
501 signing_key
502 ''}" \
503 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
504 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
505 --load-privkey "${cfg.dataDir}/keys/server.key" \
506 --outfile "${cfg.dataDir}/keys/server.cert"
507
508 chgrp "${cfg.group}" \
509 "${cfg.dataDir}/keys/server.key" \
510 "${cfg.dataDir}/keys/server.cert"
511
512 chmod g+r \
513 "${cfg.dataDir}/keys/server.key" \
514 "${cfg.dataDir}/keys/server.cert"
515 fi
516
517 if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then
518 silent_certtool --generate-crl \
519 --template "${pkgs.writeText "taskserver-crl.template" ''
520 expiration_days = ${toString cfg.pki.auto.expiration.crl}
521 ''}" \
522 --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
523 --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
524 --outfile "${cfg.dataDir}/keys/server.crl"
525
526 chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl"
527 chmod g+r "${cfg.dataDir}/keys/server.crl"
528 fi
529
530 chmod go+x "${cfg.dataDir}/keys"
531 '';
532 };
533 })
534 (mkIf (cfg.enable && cfg.listenHost != "localhost") {
535 networking.firewall.allowedTCPPorts = [ cfg.listenPort ];
536 })
537 ];
538
539 meta.doc = ./doc.xml;
540}