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