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