From da53312f5c4ba1c8f4c54028b250596295798bd8 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 7 May 2015 17:49:01 +0200 Subject: [PATCH 01/60] Add services file for taskwarrior server service --- nixos/modules/services/misc/taskserver.nix | 208 +++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 nixos/modules/services/misc/taskserver.nix diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix new file mode 100644 index 000000000000..d4948e39f9ea --- /dev/null +++ b/nixos/modules/services/misc/taskserver.nix @@ -0,0 +1,208 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.taskserver; +in { + + options = { + services.taskserver = { + + enable = mkEnableOption "Taskwarrior server."; + + user = mkOption { + default = "taskd"; + description = "User for taskserver."; + }; + + dataDir = mkOption { + default = "/var/lib/taskserver/data/"; + description = "Data directory for taskserver."; + type = types.path; + }; + + caCert = mkOption { + description = "Fully qualified path to the CA certificate. Optional."; + type = types.path; + }; + + ciphers = mkOption { + default = "NORMAL"; + description = '' + List of GnuTLS ciphers to use. See your + GnuTLS documentation for full details. + ''; + type = types.string; + }; + + confirmation = mkOption { + default = true; + description = '' + Determines whether certain commands are confirmed. + ''; + type = types.bool; + }; + + debug = mkOption { + default = false; + description = '' + Logs debugging information. + ''; + type = types.bool; + }; + + extensions = mkOption { + description = '' + Fully qualified path of the Taskserver extension scripts. Currently + there are none. + ''; + type = types.path; + }; + + ipLog = mkOption { + default = true; + description = '' + Logs the IP addresses of incoming requests. + ''; + type = types.bool; + }; + + log = mkOption { + default = "/tmp/taskd.log"; + description = '' + Fully-qualified path name to the Taskserver log file. + ''; + type = types.string; + }; + + pidFile = mkOption { + default = "/tmp/taskd.pid"; + description = '' + Fully-qualified path name to the Taskserver PID file. This is used + by the 'taskdctl' script to start/stop the daemon. + ''; + type = types.string; + }; + + queueSize = mkOption { + default = 10; + description = '' + Size of the connection backlog. See 'man listen'. + ''; + type = types.int; + }; + + requestLimit = mkOption { + default = 1048576; + description = '' + Size limit of incoming requests, in bytes. + ''; + type = types.int; + }; + + client = { + + allow = mkOption { + default = [ "[Tt]ask [2-9]+" ]; + description = '' + A comma-separated list of regular expressions that are matched + against the reported client id (such as "task 2.3.0"). The values + 'all' or 'none' have special meaning. Overidden by any + 'client.deny' entry. + ''; + type = types.listOf types.str; + }; + + cert = mkOption { + description = '' + Fully qualified path of the client cert. This is used by the + 'client' command. + ''; + type = types.path; + }; + + deny = mkOption { + default = [ "[Tt]ask [2-9]+" ]; + description = '' + A comma-separated list of regular expressions that are matched + against the reported client id (such as "task 2.3.0"). The values + 'all' or 'none' have special meaning. Any 'client.deny' entry + overrides any 'client.allow' entry. + ''; + type = types.listOf types.str; + }; + + }; + + server = { + host = mkOption { + default = "localhost"; + description = '' + The address (IPv4, IPv6 or DNS) of the Taskserver. + ''; + type = types.string; + }; + + port = mkOption { + default = 53589; + description = '' + Portnumber of the Taskserver. + ''; + type = types.int; + }; + + cert = mkOption { + description = "Fully qualified path to the server certificate"; + type = types.path; + }; + + crl = mkOption { + description = '' + Fully qualified path to the server certificate + revocation list. + ''; + type = types.path; + }; + + key = mkOption { + description = '' + Fully qualified path to the server key. + + Note that sending the HUP signal to the Taskserver + causes a configuration file reload before the next + request is handled. + ''; + type = types.path; + }; + }; + }; + }; + + config = mkIf cfg.enable { + + environment.systemPackages = [ pkgs.taskserver ]; + + systemd.services.taskserver = { + description = "taskserver Service."; + path = [ pkgs.taskserver ]; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + preStart = '' + mkdir -p ${cfg.dataDir} + ''; + + environment = { + TASKDDATA = "${cfg.dataDir}"; + }; + + serviceConfig = { + ExecStart = "${pkgs.taskserver}/bin/taskdctl start"; + ExecStop = "${pkgs.taskserver}/bin/taskdctl stop"; + User = cfg.user; + }; + }; + }; +} From e6ace2a76ad7195e77629ac6c4846747ce23985f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sun, 27 Sep 2015 12:43:53 +0200 Subject: [PATCH 02/60] taskd service: Add initialization script --- nixos/modules/services/misc/taskserver.nix | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index d4948e39f9ea..ea79fae99f3a 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -191,7 +191,28 @@ in { after = [ "network.target" ]; preStart = '' - mkdir -p ${cfg.dataDir} + mkdir -p "${cfg.dataDir}" + + if [[ ! -e "${cfg.dataDir}/.is_initialized" ]] + then + ${pkgs.taskserver}/bin/taskd init + ${pkgs.taskserver}/pki/generate + for file in {{client,server}.{cert,key},server.crl,ca.cert} + do + cp $file.pem "${cfg.dataDir}/" + ${pkgs.taskserver}/bin/taskd config --force \ + $file "${cfg.dataDir}/$file.pem" + done + + ${pkgs.taskserver}/bin/taskd config --force log "${cfg.log}" + ${pkgs.taskserver}/bin/taskd config --force pid.file "${cfg.pidFile}" + ${pkgs.taskserver}/bin/taskd config --force server ${cfg.server.host}:${toString cfg.server.port} + + touch "${cfg.dataDir}/.is_initialized" + else + # already initialized + echo "Taskd was initialized. Not initializing again" + fi ''; environment = { From 80ae0fe9a21b6c9d1215bdef2d2284444432f21c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 14 May 2015 11:14:23 +0200 Subject: [PATCH 03/60] Add taskserver to module-list --- nixos/modules/module-list.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fd479763c0a0..d5922057ad48 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -249,6 +249,7 @@ ./services/misc/sundtek.nix ./services/misc/svnserve.nix ./services/misc/synergy.nix + ./services/misc/taskserver.nix ./services/misc/uhub.nix ./services/misc/zookeeper.nix ./services/monitoring/apcupsd.nix From 5442f22d0561ad7d83aa1c0889eb5d4c438101f3 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sun, 27 Sep 2015 12:47:15 +0200 Subject: [PATCH 04/60] Add taskserver to ids.nix --- nixos/modules/misc/ids.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix index 1e14fe655fc0..19a338a766e4 100644 --- a/nixos/modules/misc/ids.nix +++ b/nixos/modules/misc/ids.nix @@ -489,6 +489,7 @@ radicale = 234; syncthing = 237; #mfi = 238; # unused + taskserver = 239; # When adding a gid, make sure it doesn't match an existing # uid. Users and groups with the same name should have equal From 743993f4be094ffdb09e503b2fb85251c58acd15 Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 27 Sep 2015 15:33:30 +0200 Subject: [PATCH 05/60] nixos/ids: Rename uid and add gid for "taskd" I'm renaming the attribute name for uid, because the user name is called "taskd" so we should really use the same name for it. Signed-off-by: aszlig --- nixos/modules/misc/ids.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix index 19a338a766e4..41bcb4d751fa 100644 --- a/nixos/modules/misc/ids.nix +++ b/nixos/modules/misc/ids.nix @@ -259,6 +259,7 @@ hydra-www = 236; syncthing = 237; mfi = 238; + taskd = 239; # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399! @@ -489,7 +490,7 @@ radicale = 234; syncthing = 237; #mfi = 238; # unused - taskserver = 239; + taskd = 239; # When adding a gid, make sure it doesn't match an existing # uid. Users and groups with the same name should have equal From 5060ee456c11e80fef4a9b9106974fb5f374a271 Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 27 Sep 2015 15:35:42 +0200 Subject: [PATCH 06/60] nixos/taskserver: Unify taskd user and group The service doesn't start with the "taskd" user being present, so we really should add it. And while at it, it really makes sense to add a default group as well. I'm using a check for the user/group name as well, to allow the taskserver to be run as an existing user. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index ea79fae99f3a..519b6bb30fd9 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -16,6 +16,11 @@ in { description = "User for taskserver."; }; + group = mkOption { + default = "taskd"; + description = "Group for taskserver."; + }; + dataDir = mkOption { default = "/var/lib/taskserver/data/"; description = "Data directory for taskserver."; @@ -183,6 +188,18 @@ in { environment.systemPackages = [ pkgs.taskserver ]; + users.users = optional (cfg.user == "taskd") { + name = "taskd"; + uid = config.ids.uids.taskd; + description = "Taskserver user"; + group = cfg.group; + }; + + users.groups = optional (cfg.group == "taskd") { + name = "taskd"; + gid = config.ids.gids.taskd; + }; + systemd.services.taskserver = { description = "taskserver Service."; path = [ pkgs.taskserver ]; @@ -223,6 +240,7 @@ in { ExecStart = "${pkgs.taskserver}/bin/taskdctl start"; ExecStop = "${pkgs.taskserver}/bin/taskdctl stop"; User = cfg.user; + Group = cfg.group; }; }; }; From 8081c791e97b05e916a7c8697d3bf795fea5b407 Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 27 Sep 2015 19:25:06 +0200 Subject: [PATCH 07/60] nixos/taskserver: Remove options for log/pidFile We're aiming for a proper integration into systemd/journald, so we really don't want zillions of separate log files flying around in our system. Same as with the pidFile. The latter is only needed for taskdctl, which is a SysV-style initscript and all of its functionality plus a lot more is handled by systemd already. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 519b6bb30fd9..73d9ef567f45 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -73,23 +73,6 @@ in { type = types.bool; }; - log = mkOption { - default = "/tmp/taskd.log"; - description = '' - Fully-qualified path name to the Taskserver log file. - ''; - type = types.string; - }; - - pidFile = mkOption { - default = "/tmp/taskd.pid"; - description = '' - Fully-qualified path name to the Taskserver PID file. This is used - by the 'taskdctl' script to start/stop the daemon. - ''; - type = types.string; - }; - queueSize = mkOption { default = 10; description = '' @@ -221,8 +204,6 @@ in { $file "${cfg.dataDir}/$file.pem" done - ${pkgs.taskserver}/bin/taskd config --force log "${cfg.log}" - ${pkgs.taskserver}/bin/taskd config --force pid.file "${cfg.pidFile}" ${pkgs.taskserver}/bin/taskd config --force server ${cfg.server.host}:${toString cfg.server.port} touch "${cfg.dataDir}/.is_initialized" From 6d38a59c2d87dfdb8a80d38d5f74b3c692c1ce3b Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 27 Sep 2015 19:30:02 +0200 Subject: [PATCH 08/60] nixos/taskserver: Improve module options The descriptions for the options previously seem to be from the taskdrc(5) manual page. So in cases where they didn't make sense for us I changed the wording a bit (for example for client.deny we don't have a "comma-separated list". Also, I've reordered things a bit for consistency (type, default, example and then description) and add missing types, examples and docbook tags. Options that are not used by default now have a null value, so that we can generate a configuration file out of all the options defined for the module. The dataDir default value is now /var/lib/taskserver, because it doesn't make sense to put just yet another empty subdirectory in it and "data" doesn't quite make sense anyway, because it also contains the configuration file as well. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 114 ++++++++++++--------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 73d9ef567f45..3be547863e5d 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -9,159 +9,175 @@ in { options = { services.taskserver = { - enable = mkEnableOption "Taskwarrior server."; + enable = mkEnableOption "the Taskwarrior server"; user = mkOption { + type = types.str; default = "taskd"; - description = "User for taskserver."; + description = "User for Taskserver."; }; group = mkOption { + type = types.str; default = "taskd"; - description = "Group for taskserver."; + description = "Group for Taskserver."; }; dataDir = mkOption { - default = "/var/lib/taskserver/data/"; - description = "Data directory for taskserver."; type = types.path; + default = "/var/lib/taskserver"; + description = "Data directory for Taskserver."; }; caCert = mkOption { - description = "Fully qualified path to the CA certificate. Optional."; - type = types.path; + type = types.nullOr types.path; + default = null; + description = "Fully qualified path to the CA certificate."; }; ciphers = mkOption { - default = "NORMAL"; + type = types.nullOr types.string; + default = null; + example = "NORMAL"; description = '' - List of GnuTLS ciphers to use. See your - GnuTLS documentation for full details. + List of GnuTLS ciphers to use. See the GnuTLS documentation for full + details. ''; - type = types.string; }; confirmation = mkOption { + type = types.bool; default = true; description = '' Determines whether certain commands are confirmed. ''; - type = types.bool; }; debug = mkOption { + type = types.bool; default = false; description = '' Logs debugging information. ''; - type = types.bool; }; extensions = mkOption { + type = types.nullOr types.path; + default = null; description = '' - Fully qualified path of the Taskserver extension scripts. Currently - there are none. + Fully qualified path of the Taskserver extension scripts. + Currently there are none. ''; - type = types.path; }; ipLog = mkOption { - default = true; + type = types.bool; + default = false; description = '' Logs the IP addresses of incoming requests. ''; - type = types.bool; }; queueSize = mkOption { + type = types.int; default = 10; description = '' - Size of the connection backlog. See 'man listen'. + Size of the connection backlog, see + listen + 2 + . ''; - type = types.int; }; requestLimit = mkOption { + type = types.int; default = 1048576; description = '' Size limit of incoming requests, in bytes. ''; - type = types.int; }; client = { allow = mkOption { - default = [ "[Tt]ask [2-9]+" ]; - description = '' - A comma-separated list of regular expressions that are matched - against the reported client id (such as "task 2.3.0"). The values - 'all' or 'none' have special meaning. Overidden by any - 'client.deny' entry. - ''; type = types.listOf types.str; + default = []; + example = [ "[Tt]ask [2-9]+" ]; + description = '' + A list of regular expressions that are matched against the reported + client id (such as task 2.3.0). + + The values all or none have + special meaning. Overidden by any entry in the option + . + ''; }; cert = mkOption { + type = types.nullOr types.path; + default = null; description = '' - Fully qualified path of the client cert. This is used by the - 'client' command. + Fully qualified path of the client cert. This is used by the + client command. ''; - type = types.path; }; deny = mkOption { - default = [ "[Tt]ask [2-9]+" ]; - description = '' - A comma-separated list of regular expressions that are matched - against the reported client id (such as "task 2.3.0"). The values - 'all' or 'none' have special meaning. Any 'client.deny' entry - overrides any 'client.allow' entry. - ''; type = types.listOf types.str; + default = []; + example = [ "[Tt]ask [2-9]+" ]; + description = '' + A list of regular expressions that are matched against the reported + client id (such as task 2.3.0). + + The values all or none have + special meaning. Any entry here overrides these in + . + ''; }; }; server = { host = mkOption { + type = types.string; default = "localhost"; description = '' The address (IPv4, IPv6 or DNS) of the Taskserver. ''; - type = types.string; }; port = mkOption { + type = types.int; default = 53589; description = '' - Portnumber of the Taskserver. + Port number of the Taskserver. ''; - type = types.int; }; cert = mkOption { + type = types.nullOr types.path; + default = null; description = "Fully qualified path to the server certificate"; - type = types.path; }; crl = mkOption { + type = types.nullOr types.path; + default = null; description = '' - Fully qualified path to the server certificate - revocation list. + Fully qualified path to the server certificate revocation list. ''; - type = types.path; }; key = mkOption { + type = types.nullOr types.path; + default = null; description = '' Fully qualified path to the server key. - Note that sending the HUP signal to the Taskserver - causes a configuration file reload before the next - request is handled. + Note that reloading the taskserver.service causes + a configuration file reload before the next request is handled. ''; - type = types.path; }; }; }; From 3d820d5ba1465929abe50fb521252ae05c9ad41c Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 27 Sep 2015 21:52:55 +0200 Subject: [PATCH 09/60] nixos/taskserver: Refactor module for CA creation Now the service starts up if only the services.taskserver.enable option is set to true. We now also have three systemd services (started in this order): * taskserver-init: For creating the necessary data directory and also includes a refecence to the configuration file in the Nix store. * taskserver-ca: Only enabled if none of the server.key, server.cert, server.crl and caCert options are set, so we can allow for certificates that are issued by another CA. This service creates a new CA key+certificate and a server key+certificate and signs the latter using the CA key. The permissions of these keys/certs are set quite strictly to allow only the root user to sign certificates. * taskserver: The main Taskserver service which just starts taskd. We now also log to stdout and thus to the journal. Of course, there are still a few problems left to solve, for instance: * The CA currently only signs the server certificates, so it's only usable for clients if the server doesn't validate client certs (which is kinda pointless). * Using "taskd " is currently still a bit awkward to use, so we need to properly wrap it in environment.systemPackages to set the dataDir by default. * There are still a few configuration options left to include, for example the "trust" option. * We might want to introduce an extraConfig option. * It might be useful to allow for declarative configuration of organisations and users, especially when it comes to creating client certificates. * The right signal has to be sent for the taskserver service to reload properly. * Currently the CA and server certificates are created using server.host as the common name and doesn't set additional certificate information. This could be improved by adding options that explicitly set that information. As for the config file, we might need to patch taskd to allow for setting not only --data but also a --cfgfile, which then omits the ${dataDir}/config file. We can still use the "include" directive from the file specified using --cfgfile in order to chainload ${dataDir}/config. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 166 +++++++++++++++++---- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 3be547863e5d..2dc24df4cad1 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -4,6 +4,60 @@ with lib; let cfg = config.services.taskserver; + + taskd = "${pkgs.taskserver}/bin/taskd"; + + mkVal = val: + if val == true then "true" + else if val == false then "false" + else if isList val then concatStringsSep ", " val + else toString val; + + mkConfLine = key: val: let + result = "${key} = ${mkVal val}"; + in optionalString (val != null && val != []) result; + + needToCreateCA = all isNull (with cfg; [ + server.key server.cert server.crl caCert + ]); + + configFile = pkgs.writeText "taskdrc" '' + # systemd related + daemon = false + log = - + + # logging + ${mkConfLine "debug" cfg.debug} + ${mkConfLine "ip.log" cfg.ipLog} + + # general + ${mkConfLine "ciphers" cfg.ciphers} + ${mkConfLine "confirmation" cfg.confirmation} + ${mkConfLine "extensions" cfg.extensions} + ${mkConfLine "queue.size" cfg.queueSize} + ${mkConfLine "request.limit" cfg.requestLimit} + + # client + ${mkConfLine "client.cert" cfg.client.cert} + ${mkConfLine "client.allow" cfg.client.allow} + ${mkConfLine "client.deny" cfg.client.deny} + + # server + server = ${cfg.server.host}:${toString cfg.server.port} + ${mkConfLine "server.crl" cfg.server.crl} + + # certificates + ${if needToCreateCA then '' + ca.cert = ${cfg.dataDir}/keys/ca.cert + server.cert = ${cfg.dataDir}/keys/server.cert + server.key = ${cfg.dataDir}/keys/server.key + '' else '' + ca.cert = ${cfg.caCert} + server.cert = ${cfg.server.cert} + server.key = ${cfg.server.key} + ''} + ''; + in { options = { @@ -199,43 +253,95 @@ in { gid = config.ids.gids.taskd; }; + systemd.services.taskserver-ca = mkIf needToCreateCA { + requiredBy = [ "taskserver.service" ]; + after = [ "taskserver-init.service" ]; + description = "Initialize CA for TaskServer"; + serviceConfig.Type = "oneshot"; + serviceConfig.UMask = "0077"; + + script = '' + mkdir -m 0700 -p "${cfg.dataDir}/keys" + chown root:root "${cfg.dataDir}/keys" + + if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then + ${pkgs.gnutls}/bin/certtool -p \ + --bits 2048 \ + --outfile "${cfg.dataDir}/keys/ca.key" + ${pkgs.gnutls}/bin/certtool -s \ + --template "${pkgs.writeText "taskserver-ca.template" '' + cn = ${cfg.server.host} + cert_signing_key + ca + ''}" \ + --load-privkey "${cfg.dataDir}/keys/ca.key" \ + --outfile "${cfg.dataDir}/keys/ca.cert" + + chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert" + chmod g+r "${cfg.dataDir}/keys/ca.cert" + fi + + if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then + ${pkgs.gnutls}/bin/certtool -p \ + --bits 2048 \ + --outfile "${cfg.dataDir}/keys/server.key" + + ${pkgs.gnutls}/bin/certtool -s \ + --template "${pkgs.writeText "taskserver-cert.template" '' + cn = ${cfg.server.host} + tls_www_server + encryption_key + signing_key + ''}" \ + --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ + --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ + --load-privkey "${cfg.dataDir}/keys/server.key" \ + --outfile "${cfg.dataDir}/keys/server.cert" + + chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.key" + chmod g+r "${cfg.dataDir}/keys/server.key" + chmod a+r "${cfg.dataDir}/keys/server.cert" + fi + + chmod go+x "${cfg.dataDir}/keys" + ''; + }; + + systemd.services.taskserver-init = { + requiredBy = [ "taskserver.service" ]; + description = "Initialize Taskserver Data Directory"; + + preStart = '' + mkdir -m 0770 -p "${cfg.dataDir}" + chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}" + ''; + + script = '' + ${taskd} init + echo "include ${configFile}" > "${cfg.dataDir}/config" + touch "${cfg.dataDir}/.is_initialized" + ''; + + environment.TASKDDATA = cfg.dataDir; + + unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized"; + + serviceConfig.Type = "oneshot"; + serviceConfig.User = cfg.user; + serviceConfig.Group = cfg.group; + serviceConfig.PermissionsStartOnly = true; + }; + systemd.services.taskserver = { - description = "taskserver Service."; - path = [ pkgs.taskserver ]; + description = "Taskwarrior Server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - preStart = '' - mkdir -p "${cfg.dataDir}" - - if [[ ! -e "${cfg.dataDir}/.is_initialized" ]] - then - ${pkgs.taskserver}/bin/taskd init - ${pkgs.taskserver}/pki/generate - for file in {{client,server}.{cert,key},server.crl,ca.cert} - do - cp $file.pem "${cfg.dataDir}/" - ${pkgs.taskserver}/bin/taskd config --force \ - $file "${cfg.dataDir}/$file.pem" - done - - ${pkgs.taskserver}/bin/taskd config --force server ${cfg.server.host}:${toString cfg.server.port} - - touch "${cfg.dataDir}/.is_initialized" - else - # already initialized - echo "Taskd was initialized. Not initializing again" - fi - ''; - - environment = { - TASKDDATA = "${cfg.dataDir}"; - }; + environment.TASKDDATA = cfg.dataDir; serviceConfig = { - ExecStart = "${pkgs.taskserver}/bin/taskdctl start"; - ExecStop = "${pkgs.taskserver}/bin/taskdctl stop"; + ExecStart = "@${taskd} taskd server"; User = cfg.user; Group = cfg.group; }; From 1f410934f296c12091c99eb6278e9afab6bcfcf3 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 4 Apr 2016 22:55:39 +0200 Subject: [PATCH 10/60] nixos/taskserver: Properly indent CA config lines No change in functionality, but it's easier to read when properly indented. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 2dc24df4cad1..d3ab9c80e077 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -48,13 +48,13 @@ let # certificates ${if needToCreateCA then '' - ca.cert = ${cfg.dataDir}/keys/ca.cert - server.cert = ${cfg.dataDir}/keys/server.cert - server.key = ${cfg.dataDir}/keys/server.key + ca.cert = ${cfg.dataDir}/keys/ca.cert + server.cert = ${cfg.dataDir}/keys/server.cert + server.key = ${cfg.dataDir}/keys/server.key '' else '' - ca.cert = ${cfg.caCert} - server.cert = ${cfg.server.cert} - server.key = ${cfg.server.key} + ca.cert = ${cfg.caCert} + server.cert = ${cfg.server.cert} + server.key = ${cfg.server.key} ''} ''; From 411c6f77a356700bcfe2a0035f8709d750d014f8 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 16:09:12 +0200 Subject: [PATCH 11/60] nixos/taskserver: Add trust option to config file The server starts up without that option anyway, but it complains about its value not being set. As we probably want to have access to that configuration value anyway, let's expose this via the NixOS module as well. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index d3ab9c80e077..6d9cfdbfe4c3 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -47,6 +47,7 @@ let ${mkConfLine "server.crl" cfg.server.crl} # certificates + ${mkConfLine "trust" cfg.server.trust} ${if needToCreateCA then '' ca.cert = ${cfg.dataDir}/keys/ca.cert server.cert = ${cfg.dataDir}/keys/server.cert @@ -233,6 +234,19 @@ in { a configuration file reload before the next request is handled. ''; }; + + trust = mkOption { + type = types.enum [ "allow all" "strict" ]; + default = "strict"; + description = '' + Determines how client certificates are validated. + + The value allow all performs no client + certificate validation. This is not recommended. The value + strict causes the client certificate to be + validated against a CA. + ''; + }; }; }; }; From d94ac7a454de2e979c7390b4e81ecb96ca42c040 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 16:12:55 +0200 Subject: [PATCH 12/60] nixos/taskserver: Use types.str instead of string The "string" option type has been deprecated since a long time (800f9c2), so let's not use it here. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 6d9cfdbfe4c3..ba52f2d4cd83 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -91,7 +91,7 @@ in { }; ciphers = mkOption { - type = types.nullOr types.string; + type = types.nullOr types.str; default = null; example = "NORMAL"; description = '' @@ -195,7 +195,7 @@ in { server = { host = mkOption { - type = types.string; + type = types.str; default = "localhost"; description = '' The address (IPv4, IPv6 or DNS) of the Taskserver. From 77d7545fac317e76a04d631c6565d2ef60c5c4d5 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 16:16:14 +0200 Subject: [PATCH 13/60] nixos/taskserver: Introduce a new fqdn option Using just the host for the common name *and* for listening on the port is quite a bad idea if you want to listen on something like :: or an internal IP address which is proxied/tunneled to the outside. Hence this separates host and fqdn. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index ba52f2d4cd83..7e67f2f62320 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -198,7 +198,7 @@ in { type = types.str; default = "localhost"; description = '' - The address (IPv4, IPv6 or DNS) of the Taskserver. + The address (IPv4, IPv6 or DNS) to listen on. ''; }; @@ -210,6 +210,14 @@ in { ''; }; + fqdn = mkOption { + type = types.str; + default = "localhost"; + description = '' + The fully qualified domain name of this server. + ''; + }; + cert = mkOption { type = types.nullOr types.path; default = null; @@ -284,7 +292,7 @@ in { --outfile "${cfg.dataDir}/keys/ca.key" ${pkgs.gnutls}/bin/certtool -s \ --template "${pkgs.writeText "taskserver-ca.template" '' - cn = ${cfg.server.host} + cn = ${cfg.server.fqdn} cert_signing_key ca ''}" \ @@ -302,7 +310,7 @@ in { ${pkgs.gnutls}/bin/certtool -s \ --template "${pkgs.writeText "taskserver-cert.template" '' - cn = ${cfg.server.host} + cn = ${cfg.server.fqdn} tls_www_server encryption_key signing_key From 274fe2a23bcf34bf5b1c74d29280126e7d3e4788 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 16:36:49 +0200 Subject: [PATCH 14/60] nixos/taskserver: Fix generating server cert We were generating a self-signed certificate for the server so far, which we obviously don't want. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 7e67f2f62320..b25447ccd9d1 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -308,7 +308,7 @@ in { --bits 2048 \ --outfile "${cfg.dataDir}/keys/server.key" - ${pkgs.gnutls}/bin/certtool -s \ + ${pkgs.gnutls}/bin/certtool -c \ --template "${pkgs.writeText "taskserver-cert.template" '' cn = ${cfg.server.fqdn} tls_www_server From 5146f760957f97853188a1263ce6ac43dd5d0a55 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 17:27:58 +0200 Subject: [PATCH 15/60] nixos/taskserver: Add an option for organisations We want to declaratively specify users and organisations, so let's add another module option "organisations", which allows us to specify users, groups and of course organisations. The implementation of this is not yet done and this is just to feed the boilerplate. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index b25447ccd9d1..47a11288b523 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -59,6 +59,26 @@ let ''} ''; + orgOptions = { name, ... }: { + options.users = mkOption { + type = types.uniq (types.listOf types.str); + default = []; + example = [ "alice" "bob" ]; + description = '' + A list of user names that belong to the organization. + ''; + }; + + options.groups = mkOption { + type = types.listOf types.str; + default = []; + example = [ "workers" "slackers" ]; + description = '' + A list of group names that belong to the organization. + ''; + }; + }; + in { options = { @@ -100,6 +120,19 @@ in { ''; }; + organisations = mkOption { + type = types.attrsOf (types.submodule orgOptions); + default = {}; + example.myShinyOrganisation.users = [ "alice" "bob" ]; + example.myShinyOrganisation.groups = [ "staff" "outsiders" ]; + example.yetAnotherOrganisation.users = [ "foo" "bar" ]; + description = '' + An attribute set where the keys name the organisation and the values + are a set of lists of and + . + ''; + }; + confirmation = mkOption { type = types.bool; default = true; From 227229653a414fc6fe6c387ff3e01ef506267da9 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 17:31:58 +0200 Subject: [PATCH 16/60] nixos/taskserver: Add a nixos-taskdctl command It's a helper for NixOS systems to make it easier to handle CA certificate signing, similar to what taskd provides but comes preseeded with the values from the system configuration. The tool is very limited at the moment and only allows to *add* organisations, users and groups. Deletion and suspension however is much simpler to implement, because we don't need to handle certificate signing. Another limitation is that we don't take into account whether certificates and keys are already set in the system configuration and if they're set it will fail spectacularly. For passing the commands to the taskd command, we're using a small C program which does setuid() and setgid() to the Taskserver user and group, because runuser(1) needs PAM (quite pointless if you're already root) and su(1) doesn't allow for setting the group and setgid()s to the default group of the user, so it even doesn't work in conjunction with sg(1). In summary, we now have a shiny nixos-taskdctl command, which lets us do things like: nixos-taskdctl add-org NixOS nixos-taskdctl add-user NixOS alice nixos-taskdctl export-user NixOS alice The last command writes a series of shell commands to stdout, which then can be imported on the client by piping it into a shell as well as doing it for example via SSH: ssh root@server nixos-taskdctl export-user NixOS alice | sh Of course, in terms of security we need to improve this even further so that we generate the private key on the client and just send a CSR to the server so that we don't need to push any secrets over the wire. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 326 ++++++++++++++++++++- 1 file changed, 325 insertions(+), 1 deletion(-) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 47a11288b523..00cde305efa5 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -59,6 +59,43 @@ let ''} ''; + genClientKey = '' + umask 0077 + if tmpdir="$(${pkgs.coreutils}/bin/mktemp -d)"; then + trap "rm -rf '$tmpdir'" EXIT + ${pkgs.gnutls}/bin/certtool -p --bits 2048 --outfile "$tmpdir/key" + + cat > "$tmpdir/template" <<-\ \ EOF + organization = $organisation + cn = ${cfg.server.fqdn} + tls_www_client + encryption_key + signing_key + EOF + + ${pkgs.gnutls}/bin/certtool -c \ + --load-privkey "$tmpdir/key" \ + --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ + --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ + --template "$tmpdir/template" \ + --outfile "$tmpdir/cert" + + mkdir -m 0700 -p "${cfg.dataDir}/keys/user/$organisation/$user" + chown root:root "${cfg.dataDir}/keys/user/$organisation/$user" + cat "$tmpdir/key" \ + > "${cfg.dataDir}/keys/user/$organisation/$user/private.key" + cat "$tmpdir/cert" \ + > "${cfg.dataDir}/keys/user/$organisation/$user/public.cert" + + rm -rf "$tmpdir" + trap - EXIT + else + echo "Unable to create temporary directory for client" \ + "certificate creation." >&2 + exit 1 + fi + ''; + orgOptions = { name, ... }: { options.users = mkOption { type = types.uniq (types.listOf types.str); @@ -79,6 +116,293 @@ let }; }; + mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'"; + mkShellName = replaceStrings ["-"] ["_"]; + + mkSubCommand = name: { args, description, script }: let + mkArg = pos: arg: "local ${arg}=\"\$${toString pos}\""; + mkDesc = line: "echo ${mkShellStr " ${line}"} >&2"; + usagePosArgs = concatMapStringsSep " " (a: "<${a}>") args; + in '' + subcmd_${mkShellName name}() { + ${concatImapStringsSep "\n " mkArg args} + ${script} + } + + usage_${mkShellName name}() { + echo " nixos-taskdctl ${name} ${usagePosArgs}" >&2 + ${concatMapStringsSep "\n " mkDesc description} + } + ''; + + mkCStr = val: "\"${escape ["\\" "\""] val}\""; + + taskdUser = let + runUser = pkgs.writeText "runuser.c" '' + #include + #include + #include + #include + #include + #include + #include + + int main(int argc, char **argv) { + struct passwd *userinfo; + struct group *groupinfo; + errno = 0; + if ((userinfo = getpwnam(${mkCStr cfg.user})) == NULL) { + if (errno == 0) + fputs(${mkCStr "User name `${cfg.user}' not found."}, stderr); + else + perror("getpwnam"); + return EXIT_FAILURE; + } + errno = 0; + if ((groupinfo = getgrnam(${mkCStr cfg.group})) == NULL) { + if (errno == 0) + fputs(${mkCStr "Group name `${cfg.group}' not found."}, stderr); + else + perror("getgrnam"); + return EXIT_FAILURE; + } + if (setgid(groupinfo->gr_gid) == -1) { + perror("setgid"); + return EXIT_FAILURE; + } + if (setuid(userinfo->pw_uid) == -1) { + perror("setgid"); + return EXIT_FAILURE; + } + argv[0] = "taskd"; + if (execv(${mkCStr taskd}, argv) == -1) { + perror("execv"); + return EXIT_FAILURE; + } + /* never reached */ + return EXIT_SUCCESS; + } + ''; + in pkgs.runCommand "taskd-user" {} '' + cc -Wall -std=c11 "${runUser}" -o "$out" + ''; + + subcommands = { + list-users = { + args = [ "organisation" ]; + + description = [ + "List all users belonging to the specified organisation." + ]; + + script = '' + legend "The following users exist for $organisation:" + ${pkgs.findutils}/bin/find \ + "${cfg.dataDir}/orgs/$organisation/users" \ + -mindepth 2 -maxdepth 2 -name config \ + -exec ${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' {} + + ''; + }; + + list-orgs = { + args = []; + + description = [ + "List available organisations" + ]; + + script = '' + legend "The following organisations exist:" + ${pkgs.findutils}/bin/find \ + "${cfg.dataDir}/orgs" -mindepth 1 -maxdepth 1 \ + -type d + ''; + }; + + get-uuid = { + args = [ "organisation" "user" ]; + + description = [ + "Get the UUID of the specified user belonging to the specified" + "organisation." + ]; + + script = '' + for uuid in "${cfg.dataDir}/orgs/$organisation/users"/*; do + usr="$(${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' "$uuid/config")" + if [ "$usr" = "$user" ]; then + legend "User $user has the following UUID:" + echo "$(${pkgs.coreutils}/bin/basename "$uuid")" + exit 0 + fi + done + echo "No UUID found for user $user." >&2 + exit 1 + ''; + }; + + export-user = { + args = [ "organisation" "user" ]; + + description = [ + "Export user of the specified organisation as a series of shell" + "commands that can be used on the client side to easily import" + "the certificates." + "" + "Note that the private key will be exported as well, so use this" + "with care!" + ]; + + script = '' + if ! subcmd_quiet list-users "$organisation" | grep -qxF "$user"; then + exists "User $user doesn't exist in organisation $organisation." + fi + + uuid="$(subcmd_quiet get-uuid "$organisation" "$user")" || exit 1 + + cat < "\$taskdatadir/keys/public.cert" < "\$taskdatadir/keys/private.key" < "\$taskdatadir/keys/ca.cert" <&2 + echo >&2 + usage_${mkShellName name} + exit 1 + fi + subcmd "${name}" ${cmdArgs};; + ''; + + nixos-taskdctl = pkgs.writeScriptBin "nixos-taskdctl" '' + #!${pkgs.stdenv.shell} + export TASKDDATA=${mkShellStr cfg.dataDir} + + quiet=0 + # Deliberately undocumented, because we don't want people to use this as + # it's only used in and specific to the preStart script of the Taskserver + # service. + if [ "$1" = "--service-helper" ]; then + quiet=1 + exists() { + exit 0 + } + shift + else + exists() { + echo "$@" >&2 + exit 1 + } + fi + + legend() { + if [ $quiet -eq 0 ]; then + echo "$@" >&2 + fi + } + + subcmd() { + local cmdname="''${1//-/_}" + shift + "subcmd_$cmdname" "$@" + } + + subcmd_quiet() { + local prev_quiet=$quiet + quiet=1 + subcmd "$@" + local ret=$? + quiet=$prev_quiet + return $ret + } + + ${concatStrings (mapAttrsToList mkSubCommand subcommands)} + + case "$1" in + ${concatStrings (mapAttrsToList mkCase subcommands)} + *) echo "Usage: nixos-taskdctl []" >&2 + echo >&2 + echo "A tool to manage taskserver users on NixOS" >&2 + echo >&2 + echo "The following subcommands are available:" >&2 + ${concatMapStringsSep "\n " (c: "usage_${mkShellName c}") + (attrNames subcommands)} + exit 1 + esac + ''; + + ctlcmd = "${nixos-taskdctl}/bin/nixos-taskdctl --service-helper"; + in { options = { @@ -294,7 +618,7 @@ in { config = mkIf cfg.enable { - environment.systemPackages = [ pkgs.taskserver ]; + environment.systemPackages = [ pkgs.taskserver nixos-taskdctl ]; users.users = optional (cfg.user == "taskd") { name = "taskd"; From 0141b4887d5db012ca231f4e907ffe617f3617a6 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 17:47:27 +0200 Subject: [PATCH 17/60] nixos/taskserver: Use nixos-taskdctl in preStart Finally, this is where we declaratively set up our organisations and users/groups, which looks like this in the system configuration: services.taskserver.organisations.NixOS.users = [ "alice" "bob" ]; This automatically sets up "alice" and "bob" for the "NixOS" organisation, generates the required client keys and signs it via the CA. However, we still need to use nixos-taskdctl export-user in order to import these certificates on the client. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver.nix | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver.nix index 00cde305efa5..992c13401e50 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver.nix @@ -719,8 +719,24 @@ in { environment.TASKDDATA = cfg.dataDir; + preStart = '' + ${concatStrings (mapAttrsToList (orgName: attrs: '' + ${ctlcmd} add-org ${mkShellStr orgName} + + ${concatMapStrings (user: '' + echo Creating ${user} >&2 + ${ctlcmd} add-user ${mkShellStr orgName} ${mkShellStr user} + '') attrs.users} + + ${concatMapStrings (group: '' + ${ctlcmd} add-group ${mkShellStr orgName} ${mkShellStr user} + '') attrs.groups} + '') cfg.organisations)} + ''; + serviceConfig = { ExecStart = "@${taskd} taskd server"; + PermissionsStartOnly = true; User = cfg.user; Group = cfg.group; }; From 61b8d9ebe053921a7b7ecbb1f0a252431c76d5d0 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 17:52:55 +0200 Subject: [PATCH 18/60] nixos/tests: Add a test for the Taskserver service A small test which checks whether tasks can be synced using the Taskserver. It doesn't test group functionality because I suspect that they're not yet implemented upstream. I haven't done an in-depth check on that but I couldn't find a method of linking groups to users yet so I guess this will get in with one of the text releases of Taskwarrior/Taskserver. Signed-off-by: aszlig --- nixos/release.nix | 1 + nixos/tests/taskserver.nix | 97 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 nixos/tests/taskserver.nix diff --git a/nixos/release.nix b/nixos/release.nix index 8a01b2685a78..2bccef1fd34b 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -253,6 +253,7 @@ in rec { tests.sddm = callTest tests/sddm.nix {}; tests.sddm-kde5 = callTest tests/sddm-kde5.nix {}; tests.simple = callTest tests/simple.nix {}; + tests.taskserver = callTest tests/taskserver.nix {}; tests.tomcat = callTest tests/tomcat.nix {}; tests.udisks2 = callTest tests/udisks2.nix {}; tests.virtualbox = callSubTests tests/virtualbox.nix { system = "x86_64-linux"; }; diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix new file mode 100644 index 000000000000..927dcbd9b28b --- /dev/null +++ b/nixos/tests/taskserver.nix @@ -0,0 +1,97 @@ +import ./make-test.nix { + name = "taskserver"; + + nodes = { + server = { + networking.firewall.enable = false; + services.taskserver.enable = true; + services.taskserver.server.host = "::"; + services.taskserver.server.fqdn = "server"; + services.taskserver.organisations = { + testOrganisation.users = [ "alice" "foo" ]; + anotherOrganisation.users = [ "bob" ]; + }; + }; + + client1 = { pkgs, ... }: { + networking.firewall.enable = false; + environment.systemPackages = [ pkgs.taskwarrior ]; + users.users.alice.isNormalUser = true; + users.users.bob.isNormalUser = true; + users.users.foo.isNormalUser = true; + }; + + client2 = { pkgs, ... }: { + networking.firewall.enable = false; + environment.systemPackages = [ pkgs.taskwarrior ]; + users.users.alice.isNormalUser = true; + users.users.bob.isNormalUser = true; + users.users.foo.isNormalUser = true; + }; + }; + + testScript = { nodes, ... }: let + cfg = nodes.server.config.services.taskserver; + portStr = toString cfg.server.port; + in '' + sub su ($$) { + my ($user, $cmd) = @_; + my $esc = $cmd =~ s/'/'\\${"'"}'/gr; + return "su - $user -c '$esc'"; + } + + sub setupClientsFor ($$) { + my ($org, $user) = @_; + + for my $client ($client1, $client2) { + $client->nest("initialize client for user $user", sub { + $client->succeed( + su $user, "task rc.confirmation=no config confirmation no" + ); + + my $exportinfo = $server->succeed( + "nixos-taskdctl export-user $org $user" + ); + + $exportinfo =~ s/'/'\\'''/g; + + $client->succeed(su $user, "eval '$exportinfo' >&2"); + $client->succeed(su $user, + "task config taskd.server server:${portStr} >&2" + ); + + $client->succeed(su $user, "task sync init >&2"); + }); + } + } + + startAll; + + $server->waitForUnit("taskserver.service"); + + $server->succeed( + "nixos-taskdctl list-users testOrganisation | grep -qxF alice", + "nixos-taskdctl list-users testOrganisation | grep -qxF foo", + "nixos-taskdctl list-users anotherOrganisation | grep -qxF bob" + ); + + $server->waitForOpenPort(${portStr}); + + $client1->waitForUnit("multi-user.target"); + $client2->waitForUnit("multi-user.target"); + + setupClientsFor "testOrganisation", "alice"; + setupClientsFor "testOrganisation", "foo"; + setupClientsFor "anotherOrganisation", "bob"; + + for ("alice", "bob", "foo") { + subtest "sync for $_", sub { + $client1->succeed(su $_, "task add foo >&2"); + $client1->succeed(su $_, "task sync >&2"); + $client2->fail(su $_, "task list >&2"); + $client2->succeed(su $_, "task sync >&2"); + $client2->succeed(su $_, "task list >&2"); + }; + } + ''; +} From 78925e4a90f838c0a977fe22b2c39a004b931fd3 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 18:07:21 +0200 Subject: [PATCH 19/60] nixos/taskserver: Factor out nixos-taskdctl With a cluttered up module source it's really a pain to navigate through it, so it's a good idea to put it into another file. No changes in functionality here, just splitting up the files and fixing references. Signed-off-by: aszlig --- nixos/modules/module-list.nix | 2 +- .../default.nix} | 321 +---------------- .../services/misc/taskserver/helper-tool.nix | 323 ++++++++++++++++++ 3 files changed, 327 insertions(+), 319 deletions(-) rename nixos/modules/services/misc/{taskserver.nix => taskserver/default.nix} (57%) create mode 100644 nixos/modules/services/misc/taskserver/helper-tool.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index d5922057ad48..2e1b2be403e3 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -249,7 +249,7 @@ ./services/misc/sundtek.nix ./services/misc/svnserve.nix ./services/misc/synergy.nix - ./services/misc/taskserver.nix + ./services/misc/taskserver ./services/misc/uhub.nix ./services/misc/zookeeper.nix ./services/monitoring/apcupsd.nix diff --git a/nixos/modules/services/misc/taskserver.nix b/nixos/modules/services/misc/taskserver/default.nix similarity index 57% rename from nixos/modules/services/misc/taskserver.nix rename to nixos/modules/services/misc/taskserver/default.nix index 992c13401e50..4a33ddd9d246 100644 --- a/nixos/modules/services/misc/taskserver.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -59,43 +59,6 @@ let ''} ''; - genClientKey = '' - umask 0077 - if tmpdir="$(${pkgs.coreutils}/bin/mktemp -d)"; then - trap "rm -rf '$tmpdir'" EXIT - ${pkgs.gnutls}/bin/certtool -p --bits 2048 --outfile "$tmpdir/key" - - cat > "$tmpdir/template" <<-\ \ EOF - organization = $organisation - cn = ${cfg.server.fqdn} - tls_www_client - encryption_key - signing_key - EOF - - ${pkgs.gnutls}/bin/certtool -c \ - --load-privkey "$tmpdir/key" \ - --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ - --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ - --template "$tmpdir/template" \ - --outfile "$tmpdir/cert" - - mkdir -m 0700 -p "${cfg.dataDir}/keys/user/$organisation/$user" - chown root:root "${cfg.dataDir}/keys/user/$organisation/$user" - cat "$tmpdir/key" \ - > "${cfg.dataDir}/keys/user/$organisation/$user/private.key" - cat "$tmpdir/cert" \ - > "${cfg.dataDir}/keys/user/$organisation/$user/public.cert" - - rm -rf "$tmpdir" - trap - EXIT - else - echo "Unable to create temporary directory for client" \ - "certificate creation." >&2 - exit 1 - fi - ''; - orgOptions = { name, ... }: { options.users = mkOption { type = types.uniq (types.listOf types.str); @@ -117,290 +80,12 @@ let }; mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'"; - mkShellName = replaceStrings ["-"] ["_"]; - mkSubCommand = name: { args, description, script }: let - mkArg = pos: arg: "local ${arg}=\"\$${toString pos}\""; - mkDesc = line: "echo ${mkShellStr " ${line}"} >&2"; - usagePosArgs = concatMapStringsSep " " (a: "<${a}>") args; - in '' - subcmd_${mkShellName name}() { - ${concatImapStringsSep "\n " mkArg args} - ${script} - } - - usage_${mkShellName name}() { - echo " nixos-taskdctl ${name} ${usagePosArgs}" >&2 - ${concatMapStringsSep "\n " mkDesc description} - } - ''; - - mkCStr = val: "\"${escape ["\\" "\""] val}\""; - - taskdUser = let - runUser = pkgs.writeText "runuser.c" '' - #include - #include - #include - #include - #include - #include - #include - - int main(int argc, char **argv) { - struct passwd *userinfo; - struct group *groupinfo; - errno = 0; - if ((userinfo = getpwnam(${mkCStr cfg.user})) == NULL) { - if (errno == 0) - fputs(${mkCStr "User name `${cfg.user}' not found."}, stderr); - else - perror("getpwnam"); - return EXIT_FAILURE; - } - errno = 0; - if ((groupinfo = getgrnam(${mkCStr cfg.group})) == NULL) { - if (errno == 0) - fputs(${mkCStr "Group name `${cfg.group}' not found."}, stderr); - else - perror("getgrnam"); - return EXIT_FAILURE; - } - if (setgid(groupinfo->gr_gid) == -1) { - perror("setgid"); - return EXIT_FAILURE; - } - if (setuid(userinfo->pw_uid) == -1) { - perror("setgid"); - return EXIT_FAILURE; - } - argv[0] = "taskd"; - if (execv(${mkCStr taskd}, argv) == -1) { - perror("execv"); - return EXIT_FAILURE; - } - /* never reached */ - return EXIT_SUCCESS; - } - ''; - in pkgs.runCommand "taskd-user" {} '' - cc -Wall -std=c11 "${runUser}" -o "$out" - ''; - - subcommands = { - list-users = { - args = [ "organisation" ]; - - description = [ - "List all users belonging to the specified organisation." - ]; - - script = '' - legend "The following users exist for $organisation:" - ${pkgs.findutils}/bin/find \ - "${cfg.dataDir}/orgs/$organisation/users" \ - -mindepth 2 -maxdepth 2 -name config \ - -exec ${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' {} + - ''; - }; - - list-orgs = { - args = []; - - description = [ - "List available organisations" - ]; - - script = '' - legend "The following organisations exist:" - ${pkgs.findutils}/bin/find \ - "${cfg.dataDir}/orgs" -mindepth 1 -maxdepth 1 \ - -type d - ''; - }; - - get-uuid = { - args = [ "organisation" "user" ]; - - description = [ - "Get the UUID of the specified user belonging to the specified" - "organisation." - ]; - - script = '' - for uuid in "${cfg.dataDir}/orgs/$organisation/users"/*; do - usr="$(${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' "$uuid/config")" - if [ "$usr" = "$user" ]; then - legend "User $user has the following UUID:" - echo "$(${pkgs.coreutils}/bin/basename "$uuid")" - exit 0 - fi - done - echo "No UUID found for user $user." >&2 - exit 1 - ''; - }; - - export-user = { - args = [ "organisation" "user" ]; - - description = [ - "Export user of the specified organisation as a series of shell" - "commands that can be used on the client side to easily import" - "the certificates." - "" - "Note that the private key will be exported as well, so use this" - "with care!" - ]; - - script = '' - if ! subcmd_quiet list-users "$organisation" | grep -qxF "$user"; then - exists "User $user doesn't exist in organisation $organisation." - fi - - uuid="$(subcmd_quiet get-uuid "$organisation" "$user")" || exit 1 - - cat < "\$taskdatadir/keys/public.cert" < "\$taskdatadir/keys/private.key" < "\$taskdatadir/keys/ca.cert" <&2 - echo >&2 - usage_${mkShellName name} - exit 1 - fi - subcmd "${name}" ${cmdArgs};; - ''; - - nixos-taskdctl = pkgs.writeScriptBin "nixos-taskdctl" '' - #!${pkgs.stdenv.shell} - export TASKDDATA=${mkShellStr cfg.dataDir} - - quiet=0 - # Deliberately undocumented, because we don't want people to use this as - # it's only used in and specific to the preStart script of the Taskserver - # service. - if [ "$1" = "--service-helper" ]; then - quiet=1 - exists() { - exit 0 - } - shift - else - exists() { - echo "$@" >&2 - exit 1 - } - fi - - legend() { - if [ $quiet -eq 0 ]; then - echo "$@" >&2 - fi - } - - subcmd() { - local cmdname="''${1//-/_}" - shift - "subcmd_$cmdname" "$@" - } - - subcmd_quiet() { - local prev_quiet=$quiet - quiet=1 - subcmd "$@" - local ret=$? - quiet=$prev_quiet - return $ret - } - - ${concatStrings (mapAttrsToList mkSubCommand subcommands)} - - case "$1" in - ${concatStrings (mapAttrsToList mkCase subcommands)} - *) echo "Usage: nixos-taskdctl []" >&2 - echo >&2 - echo "A tool to manage taskserver users on NixOS" >&2 - echo >&2 - echo "The following subcommands are available:" >&2 - ${concatMapStringsSep "\n " (c: "usage_${mkShellName c}") - (attrNames subcommands)} - exit 1 - esac - ''; - ctlcmd = "${nixos-taskdctl}/bin/nixos-taskdctl --service-helper"; in { diff --git a/nixos/modules/services/misc/taskserver/helper-tool.nix b/nixos/modules/services/misc/taskserver/helper-tool.nix new file mode 100644 index 000000000000..90ac85948164 --- /dev/null +++ b/nixos/modules/services/misc/taskserver/helper-tool.nix @@ -0,0 +1,323 @@ +{ config, pkgs, lib, mkShellStr, taskd }: + +let + mkShellName = lib.replaceStrings ["-"] ["_"]; + + genClientKey = '' + umask 0077 + if tmpdir="$(${pkgs.coreutils}/bin/mktemp -d)"; then + trap "rm -rf '$tmpdir'" EXIT + ${pkgs.gnutls}/bin/certtool -p --bits 2048 --outfile "$tmpdir/key" + + cat > "$tmpdir/template" <<-\ \ EOF + organization = $organisation + cn = ${config.server.fqdn} + tls_www_client + encryption_key + signing_key + EOF + + ${pkgs.gnutls}/bin/certtool -c \ + --load-privkey "$tmpdir/key" \ + --load-ca-privkey "${config.dataDir}/keys/ca.key" \ + --load-ca-certificate "${config.dataDir}/keys/ca.cert" \ + --template "$tmpdir/template" \ + --outfile "$tmpdir/cert" + + mkdir -m 0700 -p "${config.dataDir}/keys/user/$organisation/$user" + chown root:root "${config.dataDir}/keys/user/$organisation/$user" + cat "$tmpdir/key" \ + > "${config.dataDir}/keys/user/$organisation/$user/private.key" + cat "$tmpdir/cert" \ + > "${config.dataDir}/keys/user/$organisation/$user/public.cert" + + rm -rf "$tmpdir" + trap - EXIT + else + echo "Unable to create temporary directory for client" \ + "certificate creation." >&2 + exit 1 + fi + ''; + + mkSubCommand = name: { args, description, script }: let + mkArg = pos: arg: "local ${arg}=\"\$${toString pos}\""; + mkDesc = line: "echo ${mkShellStr " ${line}"} >&2"; + usagePosArgs = lib.concatMapStringsSep " " (a: "<${a}>") args; + in '' + subcmd_${mkShellName name}() { + ${lib.concatImapStringsSep "\n " mkArg args} + ${script} + } + + usage_${mkShellName name}() { + echo " nixos-taskdctl ${name} ${usagePosArgs}" >&2 + ${lib.concatMapStringsSep "\n " mkDesc description} + } + ''; + + mkCStr = val: "\"${lib.escape ["\\" "\""] val}\""; + + taskdUser = let + runUser = pkgs.writeText "runuser.c" '' + #include + #include + #include + #include + #include + #include + #include + + int main(int argc, char **argv) { + struct passwd *userinfo; + struct group *groupinfo; + errno = 0; + if ((userinfo = getpwnam(${mkCStr config.user})) == NULL) { + if (errno == 0) + fputs(${mkCStr "User name `${config.user}' not found."}, stderr); + else + perror("getpwnam"); + return EXIT_FAILURE; + } + errno = 0; + if ((groupinfo = getgrnam(${mkCStr config.group})) == NULL) { + if (errno == 0) + fputs(${mkCStr "Group name `${config.group}' not found."}, stderr); + else + perror("getgrnam"); + return EXIT_FAILURE; + } + if (setgid(groupinfo->gr_gid) == -1) { + perror("setgid"); + return EXIT_FAILURE; + } + if (setuid(userinfo->pw_uid) == -1) { + perror("setgid"); + return EXIT_FAILURE; + } + argv[0] = "taskd"; + if (execv(${mkCStr taskd}, argv) == -1) { + perror("execv"); + return EXIT_FAILURE; + } + /* never reached */ + return EXIT_SUCCESS; + } + ''; + in pkgs.runCommand "taskd-user" {} '' + cc -Wall -std=c11 "${runUser}" -o "$out" + ''; + + subcommands = { + list-users = { + args = [ "organisation" ]; + + description = [ + "List all users belonging to the specified organisation." + ]; + + script = '' + legend "The following users exist for $organisation:" + ${pkgs.findutils}/bin/find \ + "${config.dataDir}/orgs/$organisation/users" \ + -mindepth 2 -maxdepth 2 -name config \ + -exec ${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' {} + + ''; + }; + + list-orgs = { + args = []; + + description = [ + "List available organisations" + ]; + + script = '' + legend "The following organisations exist:" + ${pkgs.findutils}/bin/find \ + "${config.dataDir}/orgs" -mindepth 1 -maxdepth 1 \ + -type d + ''; + }; + + get-uuid = { + args = [ "organisation" "user" ]; + + description = [ + "Get the UUID of the specified user belonging to the specified" + "organisation." + ]; + + script = '' + for uuid in "${config.dataDir}/orgs/$organisation/users"/*; do + usr="$(${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' "$uuid/config")" + if [ "$usr" = "$user" ]; then + legend "User $user has the following UUID:" + echo "$(${pkgs.coreutils}/bin/basename "$uuid")" + exit 0 + fi + done + echo "No UUID found for user $user." >&2 + exit 1 + ''; + }; + + export-user = { + args = [ "organisation" "user" ]; + + description = [ + "Export user of the specified organisation as a series of shell" + "commands that can be used on the client side to easily import" + "the certificates." + "" + "Note that the private key will be exported as well, so use this" + "with care!" + ]; + + script = '' + if ! subcmd_quiet list-users "$organisation" | grep -qxF "$user"; then + exists "User $user doesn't exist in organisation $organisation." + fi + + uuid="$(subcmd_quiet get-uuid "$organisation" "$user")" || exit 1 + + cat < "\$taskdatadir/keys/public.cert" < "\$taskdatadir/keys/private.key" < "\$taskdatadir/keys/ca.cert" <&2 + echo >&2 + usage_${mkShellName name} + exit 1 + fi + subcmd "${name}" ${cmdArgs};; + ''; + +in pkgs.writeScriptBin "nixos-taskdctl" '' + #!${pkgs.stdenv.shell} + export TASKDDATA=${mkShellStr config.dataDir} + + quiet=0 + # Deliberately undocumented, because we don't want people to use this as + # it's only used in and specific to the preStart script of the Taskserver + # service. + if [ "$1" = "--service-helper" ]; then + quiet=1 + exists() { + exit 0 + } + shift + else + exists() { + echo "$@" >&2 + exit 1 + } + fi + + legend() { + if [ $quiet -eq 0 ]; then + echo "$@" >&2 + fi + } + + subcmd() { + local cmdname="''${1//-/_}" + shift + "subcmd_$cmdname" "$@" + } + + subcmd_quiet() { + local prev_quiet=$quiet + quiet=1 + subcmd "$@" + local ret=$? + quiet=$prev_quiet + return $ret + } + + ${lib.concatStrings (lib.mapAttrsToList mkSubCommand subcommands)} + + case "$1" in + ${lib.concatStrings (lib.mapAttrsToList mkCase subcommands)} + *) echo "Usage: nixos-taskdctl []" >&2 + echo >&2 + echo "A tool to manage taskserver users on NixOS" >&2 + echo >&2 + echo "The following subcommands are available:" >&2 + ${lib.concatMapStringsSep "\n " (c: "usage_${mkShellName c}") + (lib.attrNames subcommands)} + exit 1 + esac +'' From 2d8961705249a9a144e2f7d944e04fd938b4a2c9 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 5 Apr 2016 18:40:15 +0200 Subject: [PATCH 20/60] nixos/taskserver: Rename nixos-taskdctl Using nixos-taskserver is more verbose but less cryptic and I think it fits the purpose better because it can't be confused to be a wrapper around the taskdctl command from the upstream project as nixos-taskserver shares no commonalities with it. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 6 +++--- nixos/modules/services/misc/taskserver/helper-tool.nix | 7 ++++--- nixos/tests/taskserver.nix | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 4a33ddd9d246..793cc2aa36bb 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -81,12 +81,12 @@ let mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'"; - nixos-taskdctl = import ./helper-tool.nix { + nixos-taskserver = import ./helper-tool.nix { inherit pkgs lib mkShellStr taskd; config = cfg; }; - ctlcmd = "${nixos-taskdctl}/bin/nixos-taskdctl --service-helper"; + ctlcmd = "${nixos-taskserver}/bin/nixos-taskserver --service-helper"; in { @@ -303,7 +303,7 @@ in { config = mkIf cfg.enable { - environment.systemPackages = [ pkgs.taskserver nixos-taskdctl ]; + environment.systemPackages = [ pkgs.taskserver nixos-taskserver ]; users.users = optional (cfg.user == "taskd") { name = "taskd"; diff --git a/nixos/modules/services/misc/taskserver/helper-tool.nix b/nixos/modules/services/misc/taskserver/helper-tool.nix index 90ac85948164..70660574d04c 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.nix +++ b/nixos/modules/services/misc/taskserver/helper-tool.nix @@ -1,6 +1,7 @@ { config, pkgs, lib, mkShellStr, taskd }: let + commandName = "nixos-taskserver"; mkShellName = lib.replaceStrings ["-"] ["_"]; genClientKey = '' @@ -51,7 +52,7 @@ let } usage_${mkShellName name}() { - echo " nixos-taskdctl ${name} ${usagePosArgs}" >&2 + echo " ${commandName} ${name} ${usagePosArgs}" >&2 ${lib.concatMapStringsSep "\n " mkDesc description} } ''; @@ -265,7 +266,7 @@ let subcmd "${name}" ${cmdArgs};; ''; -in pkgs.writeScriptBin "nixos-taskdctl" '' +in pkgs.writeScriptBin commandName '' #!${pkgs.stdenv.shell} export TASKDDATA=${mkShellStr config.dataDir} @@ -311,7 +312,7 @@ in pkgs.writeScriptBin "nixos-taskdctl" '' case "$1" in ${lib.concatStrings (lib.mapAttrsToList mkCase subcommands)} - *) echo "Usage: nixos-taskdctl []" >&2 + *) echo "Usage: ${commandName} []" >&2 echo >&2 echo "A tool to manage taskserver users on NixOS" >&2 echo >&2 diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 927dcbd9b28b..61f2b06a7f74 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -50,7 +50,7 @@ import ./make-test.nix { ); my $exportinfo = $server->succeed( - "nixos-taskdctl export-user $org $user" + "nixos-taskserver export-user $org $user" ); $exportinfo =~ s/'/'\\'''/g; @@ -70,9 +70,9 @@ import ./make-test.nix { $server->waitForUnit("taskserver.service"); $server->succeed( - "nixos-taskdctl list-users testOrganisation | grep -qxF alice", - "nixos-taskdctl list-users testOrganisation | grep -qxF foo", - "nixos-taskdctl list-users anotherOrganisation | grep -qxF bob" + "nixos-taskserver list-users testOrganisation | grep -qxF alice", + "nixos-taskserver list-users testOrganisation | grep -qxF foo", + "nixos-taskserver list-users anotherOrganisation | grep -qxF bob" ); $server->waitForOpenPort(${portStr}); From 33f948c88b0a92a629d3e19cf06ccc12d8c9c1e7 Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 7 Apr 2016 12:38:02 +0200 Subject: [PATCH 21/60] nixos/taskserver: Fix type for client.{allow,deny} We already document that we allow special values such as "all" and "none", but the type doesn't represent that. So let's use an enum in conjuction with a loeOf type so that this becomes clear. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 793cc2aa36bb..9cda787ae1a4 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -197,7 +197,7 @@ in { client = { allow = mkOption { - type = types.listOf types.str; + type = with types; loeOf (either (enum ["all" "none"]) str); default = []; example = [ "[Tt]ask [2-9]+" ]; description = '' @@ -220,7 +220,7 @@ in { }; deny = mkOption { - type = types.listOf types.str; + type = with types; loeOf (either (enum ["all" "none"]) str); default = []; example = [ "[Tt]ask [2-9]+" ]; description = '' From 04fa5dcdb8a6fc6b4d9fce101c093031f4d52168 Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 7 Apr 2016 12:55:39 +0200 Subject: [PATCH 22/60] nixos/taskserver: Fix type/description for ciphers Referring to the GnuTLS documentation isn't very nice if the user has to use a search engine to find that documentation. So let's directly link to it. The type was "str" before, but it's actually a colon-separated string, so if we set options in multiple modules, the result is one concatenated string. I know there is types.envVar, which does the same as separatedString ":" but I found that it could confuse the reader of the Taskserver module. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 9cda787ae1a4..58e7377ec9c5 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -120,12 +120,14 @@ in { }; ciphers = mkOption { - type = types.nullOr types.str; + type = types.nullOr (types.separatedString ":"); default = null; - example = "NORMAL"; - description = '' - List of GnuTLS ciphers to use. See the GnuTLS documentation for full - details. + example = "NORMAL:-VERS-SSL3.0"; + description = let + url = "https://gnutls.org/manual/html_node/Priority-Strings.html"; + in '' + List of GnuTLS ciphers to use. See the GnuTLS documentation about + priority strings at for full details. ''; }; From 8b793d1916387c67f8eeb137789b1b41a1f94537 Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 7 Apr 2016 13:20:20 +0200 Subject: [PATCH 23/60] nixos/taskserver: Rename client.{allow,deny} These values match against the client IDs only, so let's rename it to something that actually reflects that. Having client.cert in the same namespace also could lead to confusion, because the client.cert setting is for the *debugging* client only. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 58e7377ec9c5..1279e548c2a1 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -39,8 +39,8 @@ let # client ${mkConfLine "client.cert" cfg.client.cert} - ${mkConfLine "client.allow" cfg.client.allow} - ${mkConfLine "client.deny" cfg.client.deny} + ${mkConfLine "client.allow" cfg.allowedClientIDs} + ${mkConfLine "client.deny" cfg.disallowedClientIDs} # server server = ${cfg.server.host}:${toString cfg.server.port} @@ -196,45 +196,41 @@ in { ''; }; - client = { + allowedClientIDs = mkOption { + type = with types; loeOf (either (enum ["all" "none"]) str); + default = []; + example = [ "[Tt]ask [2-9]+" ]; + description = '' + A list of regular expressions that are matched against the reported + client id (such as task 2.3.0). - allow = mkOption { - type = with types; loeOf (either (enum ["all" "none"]) str); - default = []; - example = [ "[Tt]ask [2-9]+" ]; - description = '' - A list of regular expressions that are matched against the reported - client id (such as task 2.3.0). + The values all or none have + special meaning. Overidden by any entry in the option + . + ''; + }; - The values all or none have - special meaning. Overidden by any entry in the option - . - ''; - }; + disallowedClientIDs = mkOption { + type = with types; loeOf (either (enum ["all" "none"]) str); + default = []; + example = [ "[Tt]ask [2-9]+" ]; + description = '' + A list of regular expressions that are matched against the reported + client id (such as task 2.3.0). - cert = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Fully qualified path of the client cert. This is used by the - client command. - ''; - }; - - deny = mkOption { - type = with types; loeOf (either (enum ["all" "none"]) str); - default = []; - example = [ "[Tt]ask [2-9]+" ]; - description = '' - A list of regular expressions that are matched against the reported - client id (such as task 2.3.0). - - The values all or none have - special meaning. Any entry here overrides these in - . - ''; - }; + The values all or none have + special meaning. Any entry here overrides these in + . + ''; + }; + client.cert = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Fully qualified path of the client cert. This is used by the + client command. + ''; }; server = { From 64e566a49c672c289a2680c477843f4eaeb77d96 Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 7 Apr 2016 14:11:49 +0200 Subject: [PATCH 24/60] nixos/taskserver: Add module documentation It's not by any means exhaustive, but we're still going to change the implementation, so let's just use this as a starting point. Signed-off-by: aszlig --- .../manual/configuration/configuration.xml | 1 + nixos/doc/manual/default.nix | 1 + .../services/misc/taskserver/default.nix | 8 ++- .../modules/services/misc/taskserver/doc.xml | 52 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 nixos/modules/services/misc/taskserver/doc.xml diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml index caba8fb1f4ad..501ad10788c5 100644 --- a/nixos/doc/manual/configuration/configuration.xml +++ b/nixos/doc/manual/configuration/configuration.xml @@ -28,6 +28,7 @@ effect after you run nixos-rebuild. + diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 746ddc071b6a..130b016aa266 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -58,6 +58,7 @@ let cp ${../../modules/services/databases/postgresql.xml} configuration/postgresql.xml cp ${../../modules/services/misc/gitlab.xml} configuration/gitlab.xml cp ${../../modules/security/acme.xml} configuration/acme.xml + cp ${../../modules/services/misc/taskserver/doc.xml} configuration/taskserver.xml ln -s ${optionsDocBook} options-db.xml echo "${version}" > version ''; diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 1279e548c2a1..3a9669ddd266 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -88,6 +88,8 @@ let ctlcmd = "${nixos-taskserver}/bin/nixos-taskserver --service-helper"; + withMeta = meta: defs: mkMerge [ defs { inherit meta; } ]; + in { options = { @@ -299,7 +301,9 @@ in { }; }; - config = mkIf cfg.enable { + config = withMeta { + doc = ./taskserver.xml; + } (mkIf cfg.enable { environment.systemPackages = [ pkgs.taskserver nixos-taskserver ]; @@ -424,5 +428,5 @@ in { Group = cfg.group; }; }; - }; + }); } diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml new file mode 100644 index 000000000000..b41747872c66 --- /dev/null +++ b/nixos/modules/services/misc/taskserver/doc.xml @@ -0,0 +1,52 @@ + + + Taskserver + + + Taskserver is the server component of + Taskwarrior, a free and + open source todo list application. + + + + Upstream documentation: + + + +
+ Configuration + + + Taskserver does all of its authentication via TLS using client + certificates, so you either need to roll your own CA or purchase a + certificate from a known CA, which allows creation of client + certificates. + + These certificates are usually advertised as + server certificates. + + + + So in order to make it easier to handle your own CA, there is a helper + tool called nixos-taskserver which manages the custom + CA along with Taskserver users and groups. + + + + While the client certificates in Taskserver only authenticate whether a + user is allowed to connect, every user has its own UUID which identifies + it as an entity. + + + + With nixos-taskserver the client certificate is created + along with the UUID of the user, so it handles all of the credentials + needed in order to setup the Taskwarrior client to work with a Taskserver. + + + +
+
From 85832de2e8291163b62386d705b5b394a06cfac8 Mon Sep 17 00:00:00 2001 From: aszlig Date: Sun, 10 Apr 2016 21:37:12 +0200 Subject: [PATCH 25/60] nixos/taskserver: Remove client.cert option The option is solely for debugging purposes (particularly the unit tests of the project itself) and doesn't make sense to include it in the NixOS module options. If people want to use this, we might want to introduce another option so that we can insert arbitrary configuration lines. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 3a9669ddd266..62e35803117c 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -38,7 +38,6 @@ let ${mkConfLine "request.limit" cfg.requestLimit} # client - ${mkConfLine "client.cert" cfg.client.cert} ${mkConfLine "client.allow" cfg.allowedClientIDs} ${mkConfLine "client.deny" cfg.disallowedClientIDs} @@ -226,15 +225,6 @@ in { ''; }; - client.cert = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Fully qualified path of the client cert. This is used by the - client command. - ''; - }; - server = { host = mkOption { type = types.str; From 2acf8677fa85ee2d3d53de401e490e530a1049cc Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 11:52:02 +0200 Subject: [PATCH 26/60] nixos/taskserver: Rewrite helper-tool in Python In the comments of the pull request @nbp wrote: "Why is it implemented in 3 different languages: Nix, Bash and C?" And he's right, it doesn't make sense, because we were using C as a runuser replacement and used Nix to generate the shellscript boilerplates. Writing this in Python gets rid of all of this and we also don't need the boilerplate as well, because we're using Click to handle all the command line stuff. Note that this currently is a 1:1 implementation of what we had before. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 26 +- .../services/misc/taskserver/helper-tool.nix | 324 ------------------ .../services/misc/taskserver/helper-tool.py | 276 +++++++++++++++ 3 files changed, 299 insertions(+), 327 deletions(-) delete mode 100644 nixos/modules/services/misc/taskserver/helper-tool.nix create mode 100644 nixos/modules/services/misc/taskserver/helper-tool.py diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 62e35803117c..86eabb9bcfc8 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -80,9 +80,29 @@ let mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'"; - nixos-taskserver = import ./helper-tool.nix { - inherit pkgs lib mkShellStr taskd; - config = cfg; + nixos-taskserver = pkgs.buildPythonPackage { + name = "nixos-taskserver"; + namePrefix = ""; + + src = pkgs.runCommand "nixos-taskserver-src" {} '' + mkdir -p "$out" + cat "${pkgs.substituteAll { + src = ./helper-tool.py; + certtool = "${pkgs.gnutls}/bin/certtool"; + inherit taskd; + inherit (cfg) dataDir user group; + inherit (cfg.server) fqdn; + }}" > "$out/main.py" + cat > "$out/setup.py" < "$tmpdir/template" <<-\ \ EOF - organization = $organisation - cn = ${config.server.fqdn} - tls_www_client - encryption_key - signing_key - EOF - - ${pkgs.gnutls}/bin/certtool -c \ - --load-privkey "$tmpdir/key" \ - --load-ca-privkey "${config.dataDir}/keys/ca.key" \ - --load-ca-certificate "${config.dataDir}/keys/ca.cert" \ - --template "$tmpdir/template" \ - --outfile "$tmpdir/cert" - - mkdir -m 0700 -p "${config.dataDir}/keys/user/$organisation/$user" - chown root:root "${config.dataDir}/keys/user/$organisation/$user" - cat "$tmpdir/key" \ - > "${config.dataDir}/keys/user/$organisation/$user/private.key" - cat "$tmpdir/cert" \ - > "${config.dataDir}/keys/user/$organisation/$user/public.cert" - - rm -rf "$tmpdir" - trap - EXIT - else - echo "Unable to create temporary directory for client" \ - "certificate creation." >&2 - exit 1 - fi - ''; - - mkSubCommand = name: { args, description, script }: let - mkArg = pos: arg: "local ${arg}=\"\$${toString pos}\""; - mkDesc = line: "echo ${mkShellStr " ${line}"} >&2"; - usagePosArgs = lib.concatMapStringsSep " " (a: "<${a}>") args; - in '' - subcmd_${mkShellName name}() { - ${lib.concatImapStringsSep "\n " mkArg args} - ${script} - } - - usage_${mkShellName name}() { - echo " ${commandName} ${name} ${usagePosArgs}" >&2 - ${lib.concatMapStringsSep "\n " mkDesc description} - } - ''; - - mkCStr = val: "\"${lib.escape ["\\" "\""] val}\""; - - taskdUser = let - runUser = pkgs.writeText "runuser.c" '' - #include - #include - #include - #include - #include - #include - #include - - int main(int argc, char **argv) { - struct passwd *userinfo; - struct group *groupinfo; - errno = 0; - if ((userinfo = getpwnam(${mkCStr config.user})) == NULL) { - if (errno == 0) - fputs(${mkCStr "User name `${config.user}' not found."}, stderr); - else - perror("getpwnam"); - return EXIT_FAILURE; - } - errno = 0; - if ((groupinfo = getgrnam(${mkCStr config.group})) == NULL) { - if (errno == 0) - fputs(${mkCStr "Group name `${config.group}' not found."}, stderr); - else - perror("getgrnam"); - return EXIT_FAILURE; - } - if (setgid(groupinfo->gr_gid) == -1) { - perror("setgid"); - return EXIT_FAILURE; - } - if (setuid(userinfo->pw_uid) == -1) { - perror("setgid"); - return EXIT_FAILURE; - } - argv[0] = "taskd"; - if (execv(${mkCStr taskd}, argv) == -1) { - perror("execv"); - return EXIT_FAILURE; - } - /* never reached */ - return EXIT_SUCCESS; - } - ''; - in pkgs.runCommand "taskd-user" {} '' - cc -Wall -std=c11 "${runUser}" -o "$out" - ''; - - subcommands = { - list-users = { - args = [ "organisation" ]; - - description = [ - "List all users belonging to the specified organisation." - ]; - - script = '' - legend "The following users exist for $organisation:" - ${pkgs.findutils}/bin/find \ - "${config.dataDir}/orgs/$organisation/users" \ - -mindepth 2 -maxdepth 2 -name config \ - -exec ${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' {} + - ''; - }; - - list-orgs = { - args = []; - - description = [ - "List available organisations" - ]; - - script = '' - legend "The following organisations exist:" - ${pkgs.findutils}/bin/find \ - "${config.dataDir}/orgs" -mindepth 1 -maxdepth 1 \ - -type d - ''; - }; - - get-uuid = { - args = [ "organisation" "user" ]; - - description = [ - "Get the UUID of the specified user belonging to the specified" - "organisation." - ]; - - script = '' - for uuid in "${config.dataDir}/orgs/$organisation/users"/*; do - usr="$(${pkgs.gnused}/bin/sed -ne 's/^user *= *//p' "$uuid/config")" - if [ "$usr" = "$user" ]; then - legend "User $user has the following UUID:" - echo "$(${pkgs.coreutils}/bin/basename "$uuid")" - exit 0 - fi - done - echo "No UUID found for user $user." >&2 - exit 1 - ''; - }; - - export-user = { - args = [ "organisation" "user" ]; - - description = [ - "Export user of the specified organisation as a series of shell" - "commands that can be used on the client side to easily import" - "the certificates." - "" - "Note that the private key will be exported as well, so use this" - "with care!" - ]; - - script = '' - if ! subcmd_quiet list-users "$organisation" | grep -qxF "$user"; then - exists "User $user doesn't exist in organisation $organisation." - fi - - uuid="$(subcmd_quiet get-uuid "$organisation" "$user")" || exit 1 - - cat < "\$taskdatadir/keys/public.cert" < "\$taskdatadir/keys/private.key" < "\$taskdatadir/keys/ca.cert" <&2 - echo >&2 - usage_${mkShellName name} - exit 1 - fi - subcmd "${name}" ${cmdArgs};; - ''; - -in pkgs.writeScriptBin commandName '' - #!${pkgs.stdenv.shell} - export TASKDDATA=${mkShellStr config.dataDir} - - quiet=0 - # Deliberately undocumented, because we don't want people to use this as - # it's only used in and specific to the preStart script of the Taskserver - # service. - if [ "$1" = "--service-helper" ]; then - quiet=1 - exists() { - exit 0 - } - shift - else - exists() { - echo "$@" >&2 - exit 1 - } - fi - - legend() { - if [ $quiet -eq 0 ]; then - echo "$@" >&2 - fi - } - - subcmd() { - local cmdname="''${1//-/_}" - shift - "subcmd_$cmdname" "$@" - } - - subcmd_quiet() { - local prev_quiet=$quiet - quiet=1 - subcmd "$@" - local ret=$? - quiet=$prev_quiet - return $ret - } - - ${lib.concatStrings (lib.mapAttrsToList mkSubCommand subcommands)} - - case "$1" in - ${lib.concatStrings (lib.mapAttrsToList mkCase subcommands)} - *) echo "Usage: ${commandName} []" >&2 - echo >&2 - echo "A tool to manage taskserver users on NixOS" >&2 - echo >&2 - echo "The following subcommands are available:" >&2 - ${lib.concatMapStringsSep "\n " (c: "usage_${mkShellName c}") - (lib.attrNames subcommands)} - exit 1 - esac -'' diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py new file mode 100644 index 000000000000..3277a50cd510 --- /dev/null +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -0,0 +1,276 @@ +import grp +import pwd +import os +import re +import string +import subprocess +import sys + +from shutil import rmtree +from tempfile import NamedTemporaryFile + +import click + +CERTTOOL_COMMAND = "@certtool@" +TASKD_COMMAND = "@taskd@" +TASKD_DATA_DIR = "@dataDir@" +TASKD_USER = "@user@" +TASKD_GROUP = "@group@" +FQDN = "@fqdn@" + +RE_CONFIGUSER = re.compile(r'^\s*user\s*=(.*)$') + + +def run_as_taskd_user(): + uid = pwd.getpwnam(TASKD_USER).pw_uid + gid = grp.getgrnam(TASKD_GROUP).gr_gid + os.setgid(gid) + os.setuid(uid) + + +def taskd_cmd(cmd, *args, **kwargs): + return subprocess.call( + [TASKD_COMMAND, cmd, "--data", TASKD_DATA_DIR] + list(args), + preexec_fn=run_as_taskd_user, + **kwargs + ) + + +def label(msg): + if sys.stdout.isatty() or sys.stderr.isatty(): + sys.stderr.write(msg + "\n") + + +def mkpath(*args): + return os.path.join(TASKD_DATA_DIR, "orgs", *args) + + +def fetch_username(org, key): + for line in open(mkpath(org, "users", key, "config"), "r"): + match = RE_CONFIGUSER.match(line) + if match is None: + continue + return match.group(1).strip() + return None + + +def generate_key(org, user): + basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user) + if os.path.exists(basedir): + raise OSError("Keyfile directory for {} already exists.".format(user)) + + privkey = os.path.join(basedir, "private.key") + pubcert = os.path.join(basedir, "public.cert") + cakey = os.path.join(TASKD_DATA_DIR, "keys", "ca.key") + cacert = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert") + + try: + os.makedirs(basedir, mode=0700) + + cmd = [CERTTOOL_COMMAND, "-p", "--bits", "2048", "--outfile", privkey] + subprocess.call(cmd, preexec_fn=lambda: os.umask(0077)) + + template = NamedTemporaryFile(mode="w", prefix="certtool-template") + template.writelines(map(lambda l: l + "\n", [ + "organization = {0}".format(org), + "cn = {}".format(FQDN), + "tls_www_client", + "encryption_key", + "signing_key" + ])) + template.flush() + + cmd = [CERTTOOL_COMMAND, "-c", + "--load-privkey", privkey, + "--load-ca-privkey", cakey, + "--load-ca-certificate", cacert, + "--template", template.name, + "--outfile", pubcert] + subprocess.call(cmd, preexec_fn=lambda: os.umask(0077)) + except: + rmtree(basedir) + raise + + +def is_key_line(line, match): + return line.startswith("---") and line.lstrip("- ").startswith(match) + + +def getkey(*args): + path = os.path.join(TASKD_DATA_DIR, "keys", *args) + buf = [] + for line in open(path, "r"): + if len(buf) == 0: + if is_key_line(line, "BEGIN"): + buf.append(line) + continue + + buf.append(line) + + if is_key_line(line, "END"): + return ''.join(buf) + raise IOError("Unable to get key from {}.".format(path)) + + +def mktaskkey(cfg, path, keydata): + heredoc = 'cat > "{}" < Date: Mon, 11 Apr 2016 12:03:16 +0200 Subject: [PATCH 27/60] nixos/tests/taskserver: Test imperative users As the nixos-taskserver command can also be used to imperatively manage users, we need to test this as well. Signed-off-by: aszlig --- nixos/tests/taskserver.nix | 42 +++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 61f2b06a7f74..413c52a303ec 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -1,7 +1,7 @@ import ./make-test.nix { name = "taskserver"; - nodes = { + nodes = rec { server = { networking.firewall.enable = false; services.taskserver.enable = true; @@ -19,15 +19,10 @@ import ./make-test.nix { users.users.alice.isNormalUser = true; users.users.bob.isNormalUser = true; users.users.foo.isNormalUser = true; + users.users.bar.isNormalUser = true; }; - client2 = { pkgs, ... }: { - networking.firewall.enable = false; - environment.systemPackages = [ pkgs.taskwarrior ]; - users.users.alice.isNormalUser = true; - users.users.bob.isNormalUser = true; - users.users.foo.isNormalUser = true; - }; + client2 = client1; }; testScript = { nodes, ... }: let @@ -65,6 +60,17 @@ import ./make-test.nix { } } + sub testSync ($) { + my $user = $_[0]; + subtest "sync for user $user", sub { + $client1->succeed(su $user, "task add foo >&2"); + $client1->succeed(su $user, "task sync >&2"); + $client2->fail(su $user, "task list >&2"); + $client2->succeed(su $user, "task sync >&2"); + $client2->succeed(su $user, "task list >&2"); + }; + } + startAll; $server->waitForUnit("taskserver.service"); @@ -84,14 +90,16 @@ import ./make-test.nix { setupClientsFor "testOrganisation", "foo"; setupClientsFor "anotherOrganisation", "bob"; - for ("alice", "bob", "foo") { - subtest "sync for $_", sub { - $client1->succeed(su $_, "task add foo >&2"); - $client1->succeed(su $_, "task sync >&2"); - $client2->fail(su $_, "task list >&2"); - $client2->succeed(su $_, "task sync >&2"); - $client2->succeed(su $_, "task list >&2"); - }; - } + testSync $_ for ("alice", "bob", "foo"); + + $server->fail("nixos-taskserver add-user imperativeOrg bar"); + $server->succeed( + "nixos-taskserver add-org imperativeOrg", + "nixos-taskserver add-user imperativeOrg bar" + ); + + setupClientsFor "imperativeOrg", "bar"; + + testSync "bar"; ''; } From d6bd457d1f5514468a34c32e54076d0cf5a02122 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 12:26:34 +0200 Subject: [PATCH 28/60] nixos/taskserver: Rename server.{host,port} Having an option called services.taskserver.server.host is quite confusing because we already have "server" in the service name, so let's first get rid of the listening options before we rename the rest of the options in that .server attribute. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 34 +++++++++---------- nixos/tests/taskserver.nix | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 86eabb9bcfc8..8f760a4579d4 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -42,7 +42,7 @@ let ${mkConfLine "client.deny" cfg.disallowedClientIDs} # server - server = ${cfg.server.host}:${toString cfg.server.port} + server = ${cfg.listenHost}:${toString cfg.listenPort} ${mkConfLine "server.crl" cfg.server.crl} # certificates @@ -245,23 +245,23 @@ in { ''; }; + listenHost = mkOption { + type = types.str; + default = "localhost"; + description = '' + The address (IPv4, IPv6 or DNS) to listen on. + ''; + }; + + listenPort = mkOption { + type = types.int; + default = 53589; + description = '' + Port number of the Taskserver. + ''; + }; + server = { - host = mkOption { - type = types.str; - default = "localhost"; - description = '' - The address (IPv4, IPv6 or DNS) to listen on. - ''; - }; - - port = mkOption { - type = types.int; - default = 53589; - description = '' - Port number of the Taskserver. - ''; - }; - fqdn = mkOption { type = types.str; default = "localhost"; diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 413c52a303ec..d588b178aae8 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -5,7 +5,7 @@ import ./make-test.nix { server = { networking.firewall.enable = false; services.taskserver.enable = true; - services.taskserver.server.host = "::"; + services.taskserver.listenHost = "::"; services.taskserver.server.fqdn = "server"; services.taskserver.organisations = { testOrganisation.users = [ "alice" "foo" ]; @@ -27,7 +27,7 @@ import ./make-test.nix { testScript = { nodes, ... }: let cfg = nodes.server.config.services.taskserver; - portStr = toString cfg.server.port; + portStr = toString cfg.listenPort; in '' sub su ($$) { my ($user, $cmd) = @_; From 6de94e7d2449eefccdb99100426759472e4b14a4 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 12:38:16 +0200 Subject: [PATCH 29/60] nixos/taskserver: Rename .server options to .pki After moving out the PKI-unrelated options, let's name this a bit more appropriate, so we can finally get rid of the taskserver.server thing. This also moves taskserver.caCert to taskserver.pki.caCert, because that clearly belongs to the PKI options. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 37 +++++++++---------- nixos/tests/taskserver.nix | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 8f760a4579d4..063002167cf5 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -17,9 +17,7 @@ let result = "${key} = ${mkVal val}"; in optionalString (val != null && val != []) result; - needToCreateCA = all isNull (with cfg; [ - server.key server.cert server.crl caCert - ]); + needToCreateCA = all isNull (with cfg.pki; [ key cert crl caCert ]); configFile = pkgs.writeText "taskdrc" '' # systemd related @@ -43,18 +41,18 @@ let # server server = ${cfg.listenHost}:${toString cfg.listenPort} - ${mkConfLine "server.crl" cfg.server.crl} + ${mkConfLine "server.crl" cfg.pki.crl} # certificates - ${mkConfLine "trust" cfg.server.trust} + ${mkConfLine "trust" cfg.pki.trust} ${if needToCreateCA then '' ca.cert = ${cfg.dataDir}/keys/ca.cert server.cert = ${cfg.dataDir}/keys/server.cert server.key = ${cfg.dataDir}/keys/server.key '' else '' - ca.cert = ${cfg.caCert} - server.cert = ${cfg.server.cert} - server.key = ${cfg.server.key} + ca.cert = ${cfg.pki.caCert} + server.cert = ${cfg.pki.cert} + server.key = ${cfg.pki.key} ''} ''; @@ -91,7 +89,7 @@ let certtool = "${pkgs.gnutls}/bin/certtool"; inherit taskd; inherit (cfg) dataDir user group; - inherit (cfg.server) fqdn; + inherit (cfg.pki) fqdn; }}" > "$out/main.py" cat > "$out/setup.py" < Date: Mon, 11 Apr 2016 12:42:20 +0200 Subject: [PATCH 30/60] nixos/taskserver: Move .pki.fqdn to .fqdn It's not necessarily related to the PKI options, because this is also used for setting the server address on the Taskwarrior client. So if someone doesn't have his/her own certificates from another CA, all options that need to be adjusted are in .pki. And if someone doesn't want to bother with getting certificates from another CA, (s)he just doesn't set anything in .pki. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 25 +++++++++---------- nixos/tests/taskserver.nix | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 063002167cf5..c5c3600c1a61 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -88,8 +88,7 @@ let src = ./helper-tool.py; certtool = "${pkgs.gnutls}/bin/certtool"; inherit taskd; - inherit (cfg) dataDir user group; - inherit (cfg.pki) fqdn; + inherit (cfg) dataDir user group fqdn; }}" > "$out/main.py" cat > "$out/setup.py" < Date: Mon, 11 Apr 2016 12:47:39 +0200 Subject: [PATCH 31/60] nixos/taskserver: Move .trust out of .pki This is clearly a server configuration option and has nothing to do with certificate creation and signing, so let's move it away from the .pki namespace. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index c5c3600c1a61..4dc5027b4bda 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -44,7 +44,7 @@ let ${mkConfLine "server.crl" cfg.pki.crl} # certificates - ${mkConfLine "trust" cfg.pki.trust} + ${mkConfLine "trust" cfg.trust} ${if needToCreateCA then '' ca.cert = ${cfg.dataDir}/keys/ca.cert server.cert = ${cfg.dataDir}/keys/server.cert @@ -261,6 +261,19 @@ in { ''; }; + trust = mkOption { + type = types.enum [ "allow all" "strict" ]; + default = "strict"; + description = '' + Determines how client certificates are validated. + + The value allow all performs no client + certificate validation. This is not recommended. The value + strict causes the client certificate to be + validated against a CA. + ''; + }; + pki = { cert = mkOption { type = types.nullOr types.path; @@ -292,19 +305,6 @@ in { a configuration file reload before the next request is handled. ''; }; - - trust = mkOption { - type = types.enum [ "allow all" "strict" ]; - default = "strict"; - description = '' - Determines how client certificates are validated. - - The value allow all performs no client - certificate validation. This is not recommended. The value - strict causes the client certificate to be - validated against a CA. - ''; - }; }; }; }; From 6395c87d075810f85677227477fa26eebb2d2041 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 12:58:29 +0200 Subject: [PATCH 32/60] nixos/taskserver: Improve doc for PKI options The improvement here is just that we're adding a big here so that users of these options are aware that whenever they're setting one of these the certificates and keys are _not_ created automatically. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 4dc5027b4bda..6da516e4d15e 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -17,7 +17,35 @@ let result = "${key} = ${mkVal val}"; in optionalString (val != null && val != []) result; - needToCreateCA = all isNull (with cfg.pki; [ key cert crl caCert ]); + mkPkiOption = desc: mkOption { + type = types.nullOr types.path; + default = null; + description = desc + '' + + Setting this option will prevent automatic CA creation and handling. + + ''; + }; + + pkiOptions = { + cert = mkPkiOption '' + Fully qualified path to the server certificate. + ''; + + caCert = mkPkiOption '' + Fully qualified path to the CA certificate. + ''; + + crl = mkPkiOption '' + Fully qualified path to the server certificate revocation list. + ''; + + key = mkPkiOption '' + Fully qualified path to the server key. + ''; + }; + + needToCreateCA = all (c: isNull cfg.pki.${c}) (attrNames pkiOptions); configFile = pkgs.writeText "taskdrc" '' # systemd related @@ -274,38 +302,7 @@ in { ''; }; - pki = { - cert = mkOption { - type = types.nullOr types.path; - default = null; - description = "Fully qualified path to the server certificate"; - }; - - caCert = mkOption { - type = types.nullOr types.path; - default = null; - description = "Fully qualified path to the CA certificate."; - }; - - crl = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Fully qualified path to the server certificate revocation list. - ''; - }; - - key = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Fully qualified path to the server key. - - Note that reloading the taskserver.service causes - a configuration file reload before the next request is handled. - ''; - }; - }; + pki = pkiOptions; }; }; From 05a7cd17fc540ab8851c6c20df7c41f180582b8c Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 13:33:48 +0200 Subject: [PATCH 33/60] nixos/taskserver: Rename .pki options We're now using .pki.server.* and .pki.ca.* so that it's entirely clear what these keys/certificates are for. For example we had just .pki.key before, which doesn't really tell very much about what it's for except if you look at the option description. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 6da516e4d15e..7e6e3d3873d8 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -28,24 +28,35 @@ let }; pkiOptions = { - cert = mkPkiOption '' - Fully qualified path to the server certificate. - ''; - - caCert = mkPkiOption '' + ca.cert = mkPkiOption '' Fully qualified path to the CA certificate. ''; - crl = mkPkiOption '' + server.cert = mkPkiOption '' + Fully qualified path to the server certificate. + ''; + + server.crl = mkPkiOption '' Fully qualified path to the server certificate revocation list. ''; - key = mkPkiOption '' + server.key = mkPkiOption '' Fully qualified path to the server key. ''; }; - needToCreateCA = all (c: isNull cfg.pki.${c}) (attrNames pkiOptions); + needToCreateCA = let + notFound = path: let + dotted = concatStringsSep "." path; + in throw "Can't find option definitions for path `${dotted}'."; + findPkiDefinitions = path: attrs: let + mkSublist = key: val: let + newPath = path ++ singleton key; + in if isOption val + then attrByPath newPath (notFound newPath) cfg.pki + else findPkiDefinitions newPath val; + in flatten (mapAttrsToList mkSublist attrs); + in all isNull (findPkiDefinitions [] pkiOptions); configFile = pkgs.writeText "taskdrc" '' # systemd related @@ -69,7 +80,7 @@ let # server server = ${cfg.listenHost}:${toString cfg.listenPort} - ${mkConfLine "server.crl" cfg.pki.crl} + ${mkConfLine "server.crl" cfg.pki.server.crl} # certificates ${mkConfLine "trust" cfg.trust} @@ -78,9 +89,9 @@ let server.cert = ${cfg.dataDir}/keys/server.cert server.key = ${cfg.dataDir}/keys/server.key '' else '' - ca.cert = ${cfg.pki.caCert} - server.cert = ${cfg.pki.cert} - server.key = ${cfg.pki.key} + ca.cert = ${cfg.pki.ca.cert} + server.cert = ${cfg.pki.server.cert} + server.key = ${cfg.pki.server.key} ''} ''; From b19fdc9ec914ac176a9332feb59a7ca83ae0e3f5 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 13:38:33 +0200 Subject: [PATCH 34/60] nixos/taskserver: Set server.crl for automatic CA Currently, we don't handle this yet, but let's set it so that we cover all the options. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 7e6e3d3873d8..c81cd20b263f 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -80,18 +80,19 @@ let # server server = ${cfg.listenHost}:${toString cfg.listenPort} - ${mkConfLine "server.crl" cfg.pki.server.crl} - - # certificates ${mkConfLine "trust" cfg.trust} + + # PKI options ${if needToCreateCA then '' ca.cert = ${cfg.dataDir}/keys/ca.cert server.cert = ${cfg.dataDir}/keys/server.cert server.key = ${cfg.dataDir}/keys/server.key + server.crl = ${cfg.dataDir}/keys/server.crl '' else '' ca.cert = ${cfg.pki.ca.cert} server.cert = ${cfg.pki.server.cert} server.key = ${cfg.pki.server.key} + server.crl = ${cfg.pki.server.crl} ''} ''; From 1d77dcaed37ab47bfe2d90711c01b475a514ff25 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 18:29:00 +0200 Subject: [PATCH 35/60] nixos/doc: Allow refs from options to the manual My first attempt to do this was to just use a conditional in order to not create exact references in the manpage but create the reference in the HTML manual, as suggested by @edolstra on IRC. Later I went on to use to reference sections of the manual, but in order to do that, we need to overhaul how we generate the manual and manpages. So, that's where we are now: There is a new derivation called "manual-olinkdb", which is the olinkdb for the HTML manual, which in turn creates the olinkdb.xml file and the manual.db. The former contains the targetdoc references and the latter the specific targetptr elements. The reason why I included the olinkdb.xml verbatim is that first of all the DTD is dependent on the Docbook XSL sources and the references within the olinkdb.xml entities are relative to the current directory. So using a store path for that would end up searching for the manual.db directly in /nix/store/manual.db. Unfortunately, the that end up in the output file are relative, so for example if you're clicking on one of these within the PDF, the URL is searched in the current directory. However, the sections from the olink's text are still valid, so we could use an alternative URL for that in the future. The manual doesn't contain any links, so even referencing the relative URL shouldn't do any harm. Signed-off-by: aszlig Cc: @edolstra --- nixos/doc/manual/default.nix | 75 +++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 130b016aa266..6261f83ac8b2 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -73,6 +73,63 @@ let ''; + manualXsltprocOptions = toString [ + "--param section.autolabel 1" + "--param section.label.includes.component.label 1" + "--stringparam html.stylesheet style.css" + "--param xref.with.number.and.title 1" + "--param toc.section.depth 3" + "--stringparam admon.style ''" + "--stringparam callout.graphics.extension .gif" + "--stringparam current.docid manual" + "--param chunk.section.depth 0" + "--param chunk.first.sections 1" + "--param use.id.as.filename 1" + "--stringparam generate.toc 'book toc appendix toc'" + "--stringparam chunk.toc ${toc}" + ]; + + olinkDB = stdenv.mkDerivation { + name = "manual-olinkdb"; + + inherit sources; + + buildInputs = [ libxml2 libxslt ]; + + buildCommand = '' + ${copySources} + + xsltproc \ + ${manualXsltprocOptions} \ + --stringparam collect.xref.targets only \ + --stringparam targets.filename "$out/manual.db" \ + --nonet --xinclude \ + ${docbook5_xsl}/xml/xsl/docbook/xhtml/chunktoc.xsl \ + ./manual.xml + + # Check the validity of the man pages sources. + xmllint --noout --nonet --xinclude --noxincludenode \ + --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \ + ./man-pages.xml + + cat > "$out/olinkdb.xml" < + + ]> + + + Allows for cross-referencing olinks between the manpages + and the HTML/PDF manuals. + + + &manualtargets; + + EOF + ''; + }; + in rec { # The NixOS options in JSON format. @@ -115,18 +172,8 @@ in rec { dst=$out/share/doc/nixos mkdir -p $dst xsltproc \ - --param section.autolabel 1 \ - --param section.label.includes.component.label 1 \ - --stringparam html.stylesheet style.css \ - --param xref.with.number.and.title 1 \ - --param toc.section.depth 3 \ - --stringparam admon.style "" \ - --stringparam callout.graphics.extension .gif \ - --param chunk.section.depth 0 \ - --param chunk.first.sections 1 \ - --param use.id.as.filename 1 \ - --stringparam generate.toc "book toc appendix toc" \ - --stringparam chunk.toc ${toc} \ + ${manualXsltprocOptions} \ + --stringparam target.database.document "${olinkDB}/olinkdb.xml" \ --nonet --xinclude --output $dst/ \ ${docbook5_xsl}/xml/xsl/docbook/xhtml/chunktoc.xsl ./manual.xml @@ -158,6 +205,7 @@ in rec { dst=$out/share/doc/nixos mkdir -p $dst xmllint --xinclude manual.xml | dblatex -o $dst/manual.pdf - \ + -P target.database.document="${olinkDB}/olinkdb.xml" \ -P doc.collab.show=0 \ -P latex.output.revhistory=0 @@ -177,7 +225,7 @@ in rec { buildCommand = '' ${copySources} - # Check the validity of the manual sources. + # Check the validity of the man pages sources. xmllint --noout --nonet --xinclude --noxincludenode \ --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \ ./man-pages.xml @@ -189,6 +237,7 @@ in rec { --param man.output.base.dir "'$out/share/man/'" \ --param man.endnotes.are.numbered 0 \ --param man.break.after.slash 1 \ + --stringparam target.database.document "${olinkDB}/olinkdb.xml" \ ${docbook5_xsl}/xml/xsl/docbook/manpages/docbook.xsl \ ./man-pages.xml ''; From 7875885fb25c4a68de2c8f7256737ff81e5ab8ff Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 18:45:09 +0200 Subject: [PATCH 36/60] nixos/taskserver: Link to manual within .enable With support in place, we can now reference the Taskserver section within the NixOS manual, so that users reading the manpage of configuration.nix(5) won't miss this information. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index c81cd20b263f..b175bd6d6752 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -147,11 +147,20 @@ let withMeta = meta: defs: mkMerge [ defs { inherit meta; } ]; in { - options = { services.taskserver = { + enable = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to enable the Taskwarrior server. - enable = mkEnableOption "the Taskwarrior server"; + More instructions about NixOS in conjuction with Taskserver can be + found in the NixOS manual at + . + ''; + }; user = mkOption { type = types.str; From cf0501600abbb247585628d685f8262630c2b3c7 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 21:25:55 +0200 Subject: [PATCH 37/60] nixos/taskserver/helper: Factor out program logic The Click functions really are for the command line and should be solely used for that. What I have in mind is that instead of that crappy --service-helper argument, we should really have a new subcommand that is expecting JSON which is directly coming from the services.taskserver.organisations module option. That way we can decrease even more boilerplate and we can also ensure that organisations, users and groups get properly deleted if they're removed from the NixOS configuration. Signed-off-by: aszlig --- .../services/misc/taskserver/helper-tool.py | 263 +++++++++++++----- 1 file changed, 196 insertions(+), 67 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index 3277a50cd510..7a582f6091f8 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -19,6 +19,28 @@ TASKD_GROUP = "@group@" FQDN = "@fqdn@" RE_CONFIGUSER = re.compile(r'^\s*user\s*=(.*)$') +RE_USERKEY = re.compile(r'New user key: (.+)$', re.MULTILINE) + + +def lazyprop(fun): + """ + Decorator which only evaluates the specified function when accessed. + """ + name = '_lazy_' + fun.__name__ + + @property + def _lazy(self): + val = getattr(self, name, None) + if val is None: + val = fun(self) + setattr(self, name, val) + return val + + return _lazy + + +class TaskdError(OSError): + pass def run_as_taskd_user(): @@ -29,7 +51,16 @@ def run_as_taskd_user(): def taskd_cmd(cmd, *args, **kwargs): - return subprocess.call( + """ + Invoke taskd with the specified command with the privileges of the 'taskd' + user and 'taskd' group. + + If 'capture_stdout' is passed as a keyword argument with the value True, + the return value are the contents the command printed to stdout. + """ + capture_stdout = kwargs.pop("capture_stdout", False) + fun = subprocess.check_output if capture_stdout else subprocess.check_call + return fun( [TASKD_COMMAND, cmd, "--data", TASKD_DATA_DIR] + list(args), preexec_fn=run_as_taskd_user, **kwargs @@ -118,6 +149,141 @@ def mktaskkey(cfg, path, keydata): return heredoc + "\n" + cmd +class User(object): + def __init__(self, org, name, key): + self.__org = org + self.name = name + self.key = key + + def export(self): + pubcert = getkey(self.__org, self.name, "public.cert") + privkey = getkey(self.__org, self.name, "private.key") + cacert = getkey("ca.cert") + + keydir = "${TASKDATA:-$HOME/.task}/keys" + + credentials = '/'.join([self.__org, self.name, self.key]) + allow_unquoted = string.ascii_letters + string.digits + "/-_." + if not all((c in allow_unquoted) for c in credentials): + credentials = "'" + credentials.replace("'", r"'\''") + "'" + + script = [ + "umask 0077", + 'mkdir -p "{}"'.format(keydir), + mktaskkey("certificate", os.path.join(keydir, "public.cert"), + pubcert), + mktaskkey("key", os.path.join(keydir, "private.key"), privkey), + mktaskkey("ca", os.path.join(keydir, "ca.cert"), cacert), + "task config taskd.credentials -- {}".format(credentials) + ] + + return "\n".join(script) + "\n" + + +class Group(object): + def __init__(self, org, name): + self.__org = org + self.name = name + + +class Organisation(object): + def __init__(self, name): + self.name = name + + def add_user(self, name): + """ + Create a new user along with a certificate and key. + + Returns a 'User' object or None if the user already exists. + """ + if name not in self.users.keys(): + output = taskd_cmd("add", "user", self.name, name, + capture_stdout=True) + key = RE_USERKEY.search(output) + if key is None: + msg = "Unable to find key while creating user {}." + raise TaskdError(msg.format(name)) + + generate_key(self.name, name) + newuser = User(self.name, name, key) + self._lazy_users[name] = newuser + return newuser + return None + + def add_group(self, name): + """ + Create a new group. + + Returns a 'Group' object or None if the group already exists. + """ + if name not in self.groups.keys(): + taskd_cmd("add", "group", self.name, name) + newgroup = Group(self.name, name) + self._lazy_groups[name] = newgroup + return newgroup + return None + + def get_user(self, name): + return self.users.get(name) + + @lazyprop + def users(self): + result = {} + for key in os.listdir(mkpath(self.name, "users")): + user = fetch_username(self.name, key) + if user is not None: + result[user] = User(self.name, user, key) + return result + + def get_group(self, name): + return self.groups.get(name) + + @lazyprop + def groups(self): + result = {} + for group in os.listdir(mkpath(self.name, "groups")): + result[group] = Group(self.name, group) + return result + + +class Manager(object): + def add_org(self, name): + """ + Create a new organisation. + + Returns an 'Organisation' object or None if the organisation already + exists. + """ + if name not in self.orgs.keys(): + taskd_cmd("add", "org", name) + neworg = Organisation(name) + self._lazy_orgs[name] = neworg + return neworg + return None + + def get_org(self, name): + return self.orgs.get(name) + + @lazyprop + def orgs(self): + result = {} + for org in os.listdir(mkpath()): + result[org] = Organisation(org) + return result + + +class OrganisationType(click.ParamType): + name = 'organisation' + + def convert(self, value, param, ctx): + org = Manager().get_org(value) + if org is None: + self.fail("Organisation {} does not exist.".format(value)) + return org + +ORGANISATION = OrganisationType() + + @click.group() @click.option('--service-helper', is_flag=True) @click.pass_context @@ -129,16 +295,14 @@ def cli(ctx, service_helper): @cli.command("list-users") -@click.argument("organisation") +@click.argument("organisation", type=ORGANISATION) def list_users(organisation): """ List all users belonging to the specified organisation. """ - label("The following users exist for {}:".format(organisation)) - for key in os.listdir(mkpath(organisation, "users")): - name = fetch_username(organisation, key) - if name is not None: - sys.stdout.write(name + "\n") + label("The following users exists for {}:".format(organisation.name)) + for user in organisation.users.values(): + sys.stdout.write(user.name + "\n") @cli.command("list-orgs") @@ -147,28 +311,28 @@ def list_orgs(): List available organisations """ label("The following organisations exist:") - for org in os.listdir(mkpath()): - sys.stdout.write(org + "\n") + for org in Manager().orgs: + sys.stdout.write(org.name + "\n") @cli.command("get-uuid") -@click.argument("organisation") +@click.argument("organisation", type=ORGANISATION) @click.argument("user") def get_uuid(organisation, user): """ Get the UUID of the specified user belonging to the specified organisation. """ - for key in os.listdir(mkpath(organisation, "users")): - name = fetch_username(organisation, key) - if name is not None and name == user: - label("User {} has the following UUID:".format(name)) - sys.stdout.write(key + "\n") - return - sys.exit("No UUID found for user {}.".format(user)) + userobj = organisation.get_user(user) + if userobj is None: + msg = "User {} doesn't exist in organisation {}." + sys.exit(msg.format(userobj.name, organisation.name)) + + label("User {} has the following UUID:".format(userobj.name)) + sys.stdout.write(user.key + "\n") @cli.command("export-user") -@click.argument("organisation") +@click.argument("organisation", type=ORGANISATION) @click.argument("user") def export_user(organisation, user): """ @@ -177,38 +341,12 @@ def export_user(organisation, user): Note that the private key will be exported as well, so use this with care! """ - name = key = None - for current_key in os.listdir(mkpath(organisation, "users")): - name = fetch_username(organisation, current_key) - if name is not None and name == user: - key = current_key - break - - if name is None: + userobj = organisation.get_user(user) + if userobj is None: msg = "User {} doesn't exist in organisation {}." - sys.exit(msg.format(user, organisation)) + sys.exit(msg.format(userobj.name, organisation.name)) - pubcert = getkey(organisation, user, "public.cert") - privkey = getkey(organisation, user, "private.key") - cacert = getkey("ca.cert") - - keydir = "${TASKDATA:-$HOME/.task}/keys" - - credentials = '/'.join([organisation, user, key]) - allow_unquoted = string.ascii_letters + string.digits + "/-_." - if not all((c in allow_unquoted) for c in credentials): - credentials = "'" + credentials.replace("'", r"'\''") + "'" - - script = [ - "umask 0077", - 'mkdir -p "{}"'.format(keydir), - mktaskkey("certificate", os.path.join(keydir, "public.cert"), pubcert), - mktaskkey("key", os.path.join(keydir, "private.key"), privkey), - mktaskkey("ca", os.path.join(keydir, "ca.cert"), cacert), - "task config taskd.credentials -- {}".format(credentials) - ] - - sys.stdout.write('\n'.join(script)) + sys.stdout.write(userobj.export()) @cli.command("add-org") @@ -228,7 +366,7 @@ def add_org(obj, name): @cli.command("add-user") -@click.argument("organisation") +@click.argument("organisation", type=ORGANISATION) @click.argument("user") @click.pass_obj def add_user(obj, organisation, user): @@ -239,38 +377,29 @@ def add_user(obj, organisation, user): The client certificate along with it's public key can be shown via the 'export-user' subcommand. """ - if not os.path.exists(mkpath(organisation)): - sys.exit("Organisation {} does not exist.".format(organisation)) - - if os.path.exists(mkpath(organisation, "users")): - for key in os.listdir(mkpath(organisation, "users")): - name = fetch_username(organisation, key) - if name is not None and name == user: - if obj['is_service_helper']: - return - msg = "User {} already exists in organisation {}." - sys.exit(msg.format(user, organisation)) - - taskd_cmd("add", "user", organisation, user) - generate_key(organisation, user) + userobj = organisation.add_user(user) + if userobj is None: + if obj['is_service_helper']: + return + msg = "User {} already exists in organisation {}." + sys.exit(msg.format(user, organisation)) @cli.command("add-group") -@click.argument("organisation") +@click.argument("organisation", type=ORGANISATION) @click.argument("group") @click.pass_obj def add_group(obj, organisation, group): """ Create a group for the given organisation. """ - if os.path.exists(mkpath(organisation, "groups", group)): + userobj = organisation.add_group(group) + if userobj is None: if obj['is_service_helper']: return msg = "Group {} already exists in organisation {}." sys.exit(msg.format(group, organisation)) - taskd_cmd("add", "group", organisation, group) - if __name__ == '__main__': cli() From 6e10705754a790bcd44d1f46dfb629678750bb9b Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 22:24:58 +0200 Subject: [PATCH 38/60] nixos/taskserver: Handle declarative conf via JSON We now no longer have the stupid --service-helper option, which silences messages about already existing organisations, users or groups. Instead of that option, we now have a new subcommand called "process-json", which accepts a JSON file directly from the specified NixOS module options and creates/deletes the users accordingly. Note that this still has a two issues left to solve in this area: * Deletion is not supported yet. * If a user is created imperatively, the next run of process-json will delete it once deletion is supported. So we need to implement deletion and a way to mark organisations, users and groups as "imperatively managed". Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 20 +---- .../services/misc/taskserver/helper-tool.py | 89 +++++++++++++++---- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index b175bd6d6752..7e993627cec4 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -142,8 +142,6 @@ let propagatedBuildInputs = [ pkgs.pythonPackages.click ]; }; - ctlcmd = "${nixos-taskserver}/bin/nixos-taskserver --service-helper"; - withMeta = meta: defs: mkMerge [ defs { inherit meta; } ]; in { @@ -432,20 +430,10 @@ in { environment.TASKDDATA = cfg.dataDir; - preStart = '' - ${concatStrings (mapAttrsToList (orgName: attrs: '' - ${ctlcmd} add-org ${mkShellStr orgName} - - ${concatMapStrings (user: '' - echo Creating ${user} >&2 - ${ctlcmd} add-user ${mkShellStr orgName} ${mkShellStr user} - '') attrs.users} - - ${concatMapStrings (group: '' - ${ctlcmd} add-group ${mkShellStr orgName} ${mkShellStr user} - '') attrs.groups} - '') cfg.organisations)} - ''; + preStart = let + jsonOrgs = builtins.toJSON cfg.organisations; + jsonFile = pkgs.writeText "orgs.json" jsonOrgs; + in "${nixos-taskserver}/bin/nixos-taskserver process-json '${jsonFile}'"; serviceConfig = { ExecStart = "@${taskd} taskd server"; diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index 7a582f6091f8..c255081f5657 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -1,4 +1,5 @@ import grp +import json import pwd import os import re @@ -210,6 +211,13 @@ class Organisation(object): return newuser return None + def del_user(self, name): + """ + Delete a user and revoke its keys. + """ + sys.stderr.write("Delete user {}.".format(name)) + # TODO: deletion! + def add_group(self, name): """ Create a new group. @@ -223,6 +231,13 @@ class Organisation(object): return newgroup return None + def del_group(self, name): + """ + Delete a group. + """ + sys.stderr.write("Delete group {}.".format(name)) + # TODO: deletion! + def get_user(self, name): return self.users.get(name) @@ -261,6 +276,14 @@ class Manager(object): return neworg return None + def del_org(self, name): + """ + Delete and revoke keys of an organisation with all its users and + groups. + """ + sys.stderr.write("Delete org {}.".format(name)) + # TODO: deletion! + def get_org(self, name): return self.orgs.get(name) @@ -285,13 +308,11 @@ ORGANISATION = OrganisationType() @click.group() -@click.option('--service-helper', is_flag=True) -@click.pass_context -def cli(ctx, service_helper): +def cli(): """ Manage Taskserver users and certificates """ - ctx.obj = {'is_service_helper': service_helper} + pass @cli.command("list-users") @@ -351,14 +372,11 @@ def export_user(organisation, user): @cli.command("add-org") @click.argument("name") -@click.pass_obj -def add_org(obj, name): +def add_org(name): """ Create an organisation with the specified name. """ if os.path.exists(mkpath(name)): - if obj['is_service_helper']: - return msg = "Organisation with name {} already exists." sys.exit(msg.format(name)) @@ -368,8 +386,7 @@ def add_org(obj, name): @cli.command("add-user") @click.argument("organisation", type=ORGANISATION) @click.argument("user") -@click.pass_obj -def add_user(obj, organisation, user): +def add_user(organisation, user): """ Create a user for the given organisation along with a client certificate and print the key of the new user. @@ -379,8 +396,6 @@ def add_user(obj, organisation, user): """ userobj = organisation.add_user(user) if userobj is None: - if obj['is_service_helper']: - return msg = "User {} already exists in organisation {}." sys.exit(msg.format(user, organisation)) @@ -388,18 +403,60 @@ def add_user(obj, organisation, user): @cli.command("add-group") @click.argument("organisation", type=ORGANISATION) @click.argument("group") -@click.pass_obj -def add_group(obj, organisation, group): +def add_group(organisation, group): """ Create a group for the given organisation. """ userobj = organisation.add_group(group) if userobj is None: - if obj['is_service_helper']: - return msg = "Group {} already exists in organisation {}." sys.exit(msg.format(group, organisation)) +def add_or_delete(old, new, add_fun, del_fun): + """ + Given an 'old' and 'new' list, figure out the intersections and invoke + 'add_fun' against every element that is not in the 'old' list and 'del_fun' + against every element that is not in the 'new' list. + + Returns a tuple where the first element is the list of elements that were + added and the second element consisting of elements that were deleted. + """ + old_set = set(old) + new_set = set(new) + to_delete = old_set - new_set + to_add = new_set - old_set + for elem in to_delete: + del_fun(elem) + for elem in to_add: + add_fun(elem) + return to_add, to_delete + + +@cli.command("process-json") +@click.argument('json-file', type=click.File('rb')) +def process_json(json_file): + """ + Create and delete users, groups and organisations based on a JSON file. + + The structure of this file is exactly the same as the + 'services.taskserver.organisations' option of the NixOS module and is used + for declaratively adding and deleting users. + + Hence this subcommand is not recommended outside of the scope of the NixOS + module. + """ + data = json.load(json_file) + + mgr = Manager() + add_or_delete(mgr.orgs.keys(), data.keys(), mgr.add_org, mgr.del_org) + + for org in mgr.orgs.values(): + add_or_delete(org.users.keys(), data[org.name]['users'], + org.add_user, org.del_user) + add_or_delete(org.groups.keys(), data[org.name]['groups'], + org.add_group, org.del_group) + + if __name__ == '__main__': cli() From d0ab6179746335e17e82b81e7056374834d54f57 Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 22:59:30 +0200 Subject: [PATCH 39/60] nixos/taskserver: Constrain server cert perms It doesn't do much harm to make the server certificate world readable, because even though it's not accessible anymore via the file system, someone can still get it by simply doing a TLS handshake with the server. So this is solely for consistency. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 7e993627cec4..b0e05340e3b7 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -388,9 +388,13 @@ in { --load-privkey "${cfg.dataDir}/keys/server.key" \ --outfile "${cfg.dataDir}/keys/server.cert" - chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.key" - chmod g+r "${cfg.dataDir}/keys/server.key" - chmod a+r "${cfg.dataDir}/keys/server.cert" + chgrp "${cfg.group}" \ + "${cfg.dataDir}/keys/server.key" \ + "${cfg.dataDir}/keys/server.cert" + + chmod g+r \ + "${cfg.dataDir}/keys/server.key" \ + "${cfg.dataDir}/keys/server.cert" fi chmod go+x "${cfg.dataDir}/keys" From b6643102d61b466b0395c8f89eab3acfc2c2438d Mon Sep 17 00:00:00 2001 From: aszlig Date: Mon, 11 Apr 2016 23:05:02 +0200 Subject: [PATCH 40/60] nixos/taskserver: Generate a cert revocation list If we want to revoke client certificates and want the server to actually notice the revocation, we need to have a valid certificate revocation list. Right now the expiration_days is set to 10 years, but that's merely to actually get certtool to actually generate the CRL without trying to prompt for user input. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index b0e05340e3b7..e2a2b896ec6a 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -397,6 +397,19 @@ in { "${cfg.dataDir}/keys/server.cert" fi + if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then + ${pkgs.gnutls}/bin/certtool --generate-crl \ + --template "${pkgs.writeText "taskserver-crl.template" '' + expiration_days = 3650 + ''}" \ + --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ + --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ + --outfile "${cfg.dataDir}/keys/server.crl" + + chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl" + chmod g+r "${cfg.dataDir}/keys/server.crl" + fi + chmod go+x "${cfg.dataDir}/keys" ''; }; From 3008836feeed905908027c0d36340bc4b64246f5 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 01:04:34 +0200 Subject: [PATCH 41/60] nixos/taskserver: Add a command to reload service Unfortunately we don't have a better way to check whether the reload has been done successfully, but at least we now *can* reload it without figuring out the exact signal to send to the process. Note that on reload, Taskserver will not reload the CRL file. For that to work, a full restart needs to be done. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index e2a2b896ec6a..3a53431939bc 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -454,6 +454,7 @@ in { serviceConfig = { ExecStart = "@${taskd} taskd server"; + ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; PermissionsStartOnly = true; User = cfg.user; Group = cfg.group; From 7889fcfa41c718b52e2161e74de38a8479cd50fb Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 01:08:34 +0200 Subject: [PATCH 42/60] nixos/taskserver/helper: Implement deletion Now we finally can delete organisations, groups and users along with certificate revocation. The new subtests now make sure that the client certificate is also revoked (both when removing the whole organisation and just a single user). If we use the imperative way to add and delete users, we have to restart the Taskserver in order for the CRL to be effective. However, by using the declarative configuration we now get this for free, because removing a user will also restart the service and thus its client certificate will end up in the CRL. Signed-off-by: aszlig --- .../services/misc/taskserver/helper-tool.py | 132 +++++++++++++++--- nixos/tests/taskserver.nix | 61 +++++++- 2 files changed, 168 insertions(+), 25 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index c255081f5657..cd712332e039 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -7,6 +7,7 @@ import string import subprocess import sys +from contextlib import contextmanager from shutil import rmtree from tempfile import NamedTemporaryFile @@ -86,6 +87,19 @@ def fetch_username(org, key): return None +@contextmanager +def create_template(contents): + """ + Generate a temporary file with the specified contents as a list of strings + and yield its path as the context. + """ + template = NamedTemporaryFile(mode="w", prefix="certtool-template") + template.writelines(map(lambda l: l + "\n", contents)) + template.flush() + yield template.name + template.close() + + def generate_key(org, user): basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user) if os.path.exists(basedir): @@ -100,30 +114,57 @@ def generate_key(org, user): os.makedirs(basedir, mode=0700) cmd = [CERTTOOL_COMMAND, "-p", "--bits", "2048", "--outfile", privkey] - subprocess.call(cmd, preexec_fn=lambda: os.umask(0077)) + subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) - template = NamedTemporaryFile(mode="w", prefix="certtool-template") - template.writelines(map(lambda l: l + "\n", [ + template_data = [ "organization = {0}".format(org), "cn = {}".format(FQDN), "tls_www_client", "encryption_key", "signing_key" - ])) - template.flush() + ] - cmd = [CERTTOOL_COMMAND, "-c", - "--load-privkey", privkey, - "--load-ca-privkey", cakey, - "--load-ca-certificate", cacert, - "--template", template.name, - "--outfile", pubcert] - subprocess.call(cmd, preexec_fn=lambda: os.umask(0077)) + with create_template(template_data) as template: + cmd = [CERTTOOL_COMMAND, "-c", + "--load-privkey", privkey, + "--load-ca-privkey", cakey, + "--load-ca-certificate", cacert, + "--template", template, + "--outfile", pubcert] + subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) except: rmtree(basedir) raise +def revoke_key(org, user): + cakey = os.path.join(TASKD_DATA_DIR, "keys", "ca.key") + cacert = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert") + crl = os.path.join(TASKD_DATA_DIR, "keys", "server.crl") + + basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user) + if not os.path.exists(basedir): + raise OSError("Keyfile directory for {} doesn't exist.".format(user)) + + pubcert = os.path.join(basedir, "public.cert") + + with create_template(["expiration_days = 3650"]) as template: + oldcrl = NamedTemporaryFile(mode="wb", prefix="old-crl") + oldcrl.write(open(crl, "rb").read()) + oldcrl.flush() + cmd = [CERTTOOL_COMMAND, + "--generate-crl", + "--load-crl", oldcrl.name, + "--load-ca-privkey", cakey, + "--load-ca-certificate", cacert, + "--load-certificate", pubcert, + "--template", template, + "--outfile", crl] + subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) + oldcrl.close() + rmtree(basedir) + + def is_key_line(line, match): return line.startswith("---") and line.lstrip("- ").startswith(match) @@ -215,8 +256,13 @@ class Organisation(object): """ Delete a user and revoke its keys. """ - sys.stderr.write("Delete user {}.".format(name)) - # TODO: deletion! + if name in self.users.keys(): + # Work around https://bug.tasktools.org/browse/TD-40: + user = self.get_user(name) + rmtree(mkpath(self.name, "users", user.key)) + + revoke_key(self.name, name) + del self._lazy_users[name] def add_group(self, name): """ @@ -235,8 +281,9 @@ class Organisation(object): """ Delete a group. """ - sys.stderr.write("Delete group {}.".format(name)) - # TODO: deletion! + if name in self.users.keys(): + taskd_cmd("remove", "group", self.name, name) + del self._lazy_groups[name] def get_user(self, name): return self.users.get(name) @@ -281,8 +328,14 @@ class Manager(object): Delete and revoke keys of an organisation with all its users and groups. """ - sys.stderr.write("Delete org {}.".format(name)) - # TODO: deletion! + org = self.get_org(name) + if org is not None: + for user in org.users.keys(): + org.del_user(user) + for group in org.groups.keys(): + org.del_group(group) + taskd_cmd("remove", "org", name) + del self._lazy_orgs[name] def get_org(self, name): return self.orgs.get(name) @@ -383,6 +436,22 @@ def add_org(name): taskd_cmd("add", "org", name) +@cli.command("del-org") +@click.argument("name") +def del_org(name): + """ + Delete the organisation with the specified name. + + All of the users and groups will be deleted as well and client certificates + will be revoked. + """ + Manager().del_org(name) + msg = ("Organisation {} deleted. Be sure to restart the Taskserver" + " using 'systemctl restart taskserver.service' in order for" + " the certificate revocation to apply.") + click.echo(msg.format(name), err=True) + + @cli.command("add-user") @click.argument("organisation", type=ORGANISATION) @click.argument("user") @@ -400,6 +469,22 @@ def add_user(organisation, user): sys.exit(msg.format(user, organisation)) +@cli.command("del-user") +@click.argument("organisation", type=ORGANISATION) +@click.argument("user") +def del_user(organisation, user): + """ + Delete a user from the given organisation. + + This will also revoke the client certificate of the given user. + """ + organisation.del_user(user) + msg = ("User {} deleted. Be sure to restart the Taskserver using" + " 'systemctl restart taskserver.service' in order for the" + " certificate revocation to apply.") + click.echo(msg.format(user), err=True) + + @cli.command("add-group") @click.argument("organisation", type=ORGANISATION) @click.argument("group") @@ -413,6 +498,17 @@ def add_group(organisation, group): sys.exit(msg.format(group, organisation)) +@cli.command("del-group") +@click.argument("organisation", type=ORGANISATION) +@click.argument("group") +def del_group(organisation, group): + """ + Delete a group from the given organisation. + """ + organisation.del_group(group) + click("Group {} deleted.".format(group), err=True) + + def add_or_delete(old, new, add_fun, del_fun): """ Given an 'old' and 'new' list, figure out the intersections and invoke diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 1a9c8dfaca25..574af0aa8803 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -15,7 +15,7 @@ import ./make-test.nix { client1 = { pkgs, ... }: { networking.firewall.enable = false; - environment.systemPackages = [ pkgs.taskwarrior ]; + environment.systemPackages = [ pkgs.taskwarrior pkgs.gnutls ]; users.users.alice.isNormalUser = true; users.users.bob.isNormalUser = true; users.users.foo.isNormalUser = true; @@ -60,6 +60,22 @@ import ./make-test.nix { } } + sub restartServer { + $server->succeed("systemctl restart taskserver.service"); + $server->waitForOpenPort(${portStr}); + } + + sub readdImperativeUser { + $server->nest("(re-)add imperative user bar", sub { + $server->execute("nixos-taskserver del-org imperativeOrg"); + $server->succeed( + "nixos-taskserver add-org imperativeOrg", + "nixos-taskserver add-user imperativeOrg bar" + ); + setupClientsFor "imperativeOrg", "bar"; + }); + } + sub testSync ($) { my $user = $_[0]; subtest "sync for user $user", sub { @@ -71,6 +87,16 @@ import ./make-test.nix { }; } + sub checkClientCert ($) { + my $user = $_[0]; + my $cmd = "gnutls-cli". + " --x509cafile=/home/$user/.task/keys/ca.cert". + " --x509keyfile=/home/$user/.task/keys/private.key". + " --x509certfile=/home/$user/.task/keys/public.cert". + " --port=${portStr} server < /dev/null"; + return su $user, $cmd; + } + startAll; $server->waitForUnit("taskserver.service"); @@ -93,13 +119,34 @@ import ./make-test.nix { testSync $_ for ("alice", "bob", "foo"); $server->fail("nixos-taskserver add-user imperativeOrg bar"); - $server->succeed( - "nixos-taskserver add-org imperativeOrg", - "nixos-taskserver add-user imperativeOrg bar" - ); - - setupClientsFor "imperativeOrg", "bar"; + readdImperativeUser; testSync "bar"; + + subtest "checking certificate revocation of user bar", sub { + $client1->succeed(checkClientCert "bar"); + + $server->succeed("nixos-taskserver del-user imperativeOrg bar"); + restartServer; + + $client1->fail(checkClientCert "bar"); + + $client1->succeed(su "bar", "task add destroy everything >&2"); + $client1->fail(su "bar", "task sync >&2"); + }; + + readdImperativeUser; + + subtest "checking certificate revocation of org imperativeOrg", sub { + $client1->succeed(checkClientCert "bar"); + + $server->succeed("nixos-taskserver del-org imperativeOrg"); + restartServer; + + $client1->fail(checkClientCert "bar"); + + $client1->succeed(su "bar", "task add destroy even more >&2"); + $client1->fail(su "bar", "task sync >&2"); + }; ''; } From cfb6ce2abed2c96d0f5af268e2d22322f47831ed Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 01:49:47 +0200 Subject: [PATCH 43/60] nixos/tests/taskserver: Make tests less noisy We were putting the whole output of "nixos-taskserver export-user" from the server to the respective client and on every such operation the whole output was shown again in the test log. Now we're *only* showing these details whenever a user import fails on the client. Signed-off-by: aszlig --- nixos/tests/taskserver.nix | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 574af0aa8803..5d2e030a8f6d 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -50,7 +50,15 @@ import ./make-test.nix { $exportinfo =~ s/'/'\\'''/g; - $client->succeed(su $user, "eval '$exportinfo' >&2"); + $client->nest("importing taskwarrior configuration", sub { + my $cmd = su $user, "eval '$exportinfo' >&2"; + my ($status, $out) = $client->execute_($cmd); + if ($status != 0) { + $client->log("output: $out"); + die "command `$cmd' did not succeed (exit code $status)\n"; + } + }); + $client->succeed(su $user, "task config taskd.server server:${portStr} >&2" ); From 9586795ef27ac4d406c10c12f92fc735b5f4ff24 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 02:16:35 +0200 Subject: [PATCH 44/60] nixos/taskserver: Silence certtool everywhere We only print the output whenever there is an error, otherwise let's shut it up because it only shows information the user can gather through other means. For example by invoking certtool manually, or by just looking at private key files (the whole blurb it's outputting is in there as well). Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 22 +++++--- .../services/misc/taskserver/helper-tool.py | 54 ++++++++++++------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 3a53431939bc..dc73ad26eb6c 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -118,6 +118,8 @@ let mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'"; + certtool = "${pkgs.gnutls}/bin/certtool"; + nixos-taskserver = pkgs.buildPythonPackage { name = "nixos-taskserver"; namePrefix = ""; @@ -126,8 +128,7 @@ let mkdir -p "$out" cat "${pkgs.substituteAll { src = ./helper-tool.py; - certtool = "${pkgs.gnutls}/bin/certtool"; - inherit taskd; + inherit taskd certtool; inherit (cfg) dataDir user group fqdn; }}" > "$out/main.py" cat > "$out/setup.py" <&1)"; then + echo "GNUTLS certtool invocation failed with output:" >&2 + echo "$output" >&2 + fi + } + mkdir -m 0700 -p "${cfg.dataDir}/keys" chown root:root "${cfg.dataDir}/keys" if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then - ${pkgs.gnutls}/bin/certtool -p \ + silent_certtool -p \ --bits 2048 \ --outfile "${cfg.dataDir}/keys/ca.key" - ${pkgs.gnutls}/bin/certtool -s \ + silent_certtool -s \ --template "${pkgs.writeText "taskserver-ca.template" '' cn = ${cfg.fqdn} cert_signing_key @@ -372,11 +380,11 @@ in { fi if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then - ${pkgs.gnutls}/bin/certtool -p \ + silent_certtool -p \ --bits 2048 \ --outfile "${cfg.dataDir}/keys/server.key" - ${pkgs.gnutls}/bin/certtool -c \ + silent_certtool -c \ --template "${pkgs.writeText "taskserver-cert.template" '' cn = ${cfg.fqdn} tls_www_server @@ -398,7 +406,7 @@ in { fi if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then - ${pkgs.gnutls}/bin/certtool --generate-crl \ + silent_certtool --generate-crl \ --template "${pkgs.writeText "taskserver-crl.template" '' expiration_days = 3650 ''}" \ diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index cd712332e039..30dcfe0a7a25 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -69,6 +69,24 @@ def taskd_cmd(cmd, *args, **kwargs): ) +def certtool_cmd(*args, **kwargs): + """ + Invoke certtool from GNUTLS and return the output of the command. + + The provided arguments are added to the certtool command and keyword + arguments are added to subprocess.check_output(). + + Note that this will suppress all output of certtool and it will only be + printed whenever there is an unsuccessful return code. + """ + return subprocess.check_output( + [CERTTOOL_COMMAND] + list(args), + preexec_fn=lambda: os.umask(0077), + stderr=subprocess.STDOUT, + **kwargs + ) + + def label(msg): if sys.stdout.isatty() or sys.stderr.isatty(): sys.stderr.write(msg + "\n") @@ -113,8 +131,7 @@ def generate_key(org, user): try: os.makedirs(basedir, mode=0700) - cmd = [CERTTOOL_COMMAND, "-p", "--bits", "2048", "--outfile", privkey] - subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) + certtool_cmd("-p", "--bits", "2048", "--outfile", privkey) template_data = [ "organization = {0}".format(org), @@ -125,13 +142,14 @@ def generate_key(org, user): ] with create_template(template_data) as template: - cmd = [CERTTOOL_COMMAND, "-c", - "--load-privkey", privkey, - "--load-ca-privkey", cakey, - "--load-ca-certificate", cacert, - "--template", template, - "--outfile", pubcert] - subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) + certtool_cmd( + "-c", + "--load-privkey", privkey, + "--load-ca-privkey", cakey, + "--load-ca-certificate", cacert, + "--template", template, + "--outfile", pubcert + ) except: rmtree(basedir) raise @@ -152,15 +170,15 @@ def revoke_key(org, user): oldcrl = NamedTemporaryFile(mode="wb", prefix="old-crl") oldcrl.write(open(crl, "rb").read()) oldcrl.flush() - cmd = [CERTTOOL_COMMAND, - "--generate-crl", - "--load-crl", oldcrl.name, - "--load-ca-privkey", cakey, - "--load-ca-certificate", cacert, - "--load-certificate", pubcert, - "--template", template, - "--outfile", crl] - subprocess.check_call(cmd, preexec_fn=lambda: os.umask(0077)) + certtool_cmd( + "--generate-crl", + "--load-crl", oldcrl.name, + "--load-ca-privkey", cakey, + "--load-ca-certificate", cacert, + "--load-certificate", pubcert, + "--template", template, + "--outfile", crl + ) oldcrl.close() rmtree(basedir) From a41b109bc10e66824af5e1f150cb741f9f9399c2 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 03:42:13 +0200 Subject: [PATCH 45/60] nixos/taskserver: Don't change imperative users Whenever the nixos-taskserver tool was invoked manually for creating an organisation/group/user we now add an empty file called .imperative to the data directory. During the preStart of the Taskserver service, we use process-json which in turn now checks whether those .imperative files exist and if so, it doesn't do anything with it. This should now ensure that whenever there is a manually created user, it doesn't get killed off by the declarative configuration in case it shouldn't exist within that configuration. In addition, we also add a small subtest to check whether this is happening or not and fail if the imperatively created user got deleted by process-json. Signed-off-by: aszlig --- .../services/misc/taskserver/helper-tool.py | 69 ++++++++++++++++--- nixos/tests/taskserver.nix | 10 ++- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index 30dcfe0a7a25..512aaa4ab9f8 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -96,6 +96,28 @@ def mkpath(*args): return os.path.join(TASKD_DATA_DIR, "orgs", *args) +def mark_imperative(*path): + """ + Mark the specified path as being imperatively managed by creating an empty + file called ".imperative", so that it doesn't interfere with the + declarative configuration. + """ + open(os.path.join(mkpath(*path), ".imperative"), 'a').close() + + +def is_imperative(*path): + """ + Check whether the given path is marked as imperative, see mark_imperative() + for more information. + """ + full_path = [] + for component in path: + full_path.append(component) + if os.path.exists(os.path.join(mkpath(*full_path), ".imperative")): + return True + return False + + def fetch_username(org, key): for line in open(mkpath(org, "users", key, "config"), "r"): match = RE_CONFIGUSER.match(line) @@ -247,8 +269,9 @@ class Group(object): class Organisation(object): - def __init__(self, name): + def __init__(self, name, ignore_imperative): self.name = name + self.ignore_imperative = ignore_imperative def add_user(self, name): """ @@ -256,6 +279,8 @@ class Organisation(object): Returns a 'User' object or None if the user already exists. """ + if self.ignore_imperative and is_imperative(self.name): + return None if name not in self.users.keys(): output = taskd_cmd("add", "user", self.name, name, capture_stdout=True) @@ -265,7 +290,7 @@ class Organisation(object): raise TaskdError(msg.format(name)) generate_key(self.name, name) - newuser = User(self.name, name, key) + newuser = User(self.name, name, key.group(1)) self._lazy_users[name] = newuser return newuser return None @@ -275,8 +300,12 @@ class Organisation(object): Delete a user and revoke its keys. """ if name in self.users.keys(): - # Work around https://bug.tasktools.org/browse/TD-40: user = self.get_user(name) + if self.ignore_imperative and \ + is_imperative(self.name, "users", user.key): + return + + # Work around https://bug.tasktools.org/browse/TD-40: rmtree(mkpath(self.name, "users", user.key)) revoke_key(self.name, name) @@ -288,6 +317,8 @@ class Organisation(object): Returns a 'Group' object or None if the group already exists. """ + if self.ignore_imperative and is_imperative(self.name): + return None if name not in self.groups.keys(): taskd_cmd("add", "group", self.name, name) newgroup = Group(self.name, name) @@ -300,6 +331,9 @@ class Organisation(object): Delete a group. """ if name in self.users.keys(): + if self.ignore_imperative and \ + is_imperative(self.name, "groups", name): + return taskd_cmd("remove", "group", self.name, name) del self._lazy_groups[name] @@ -327,6 +361,16 @@ class Organisation(object): class Manager(object): + def __init__(self, ignore_imperative=False): + """ + Instantiates an organisations manager. + + If ignore_imperative is True, all actions that modify data are checked + whether they're created imperatively and if so, they will result in no + operation. + """ + self.ignore_imperative = ignore_imperative + def add_org(self, name): """ Create a new organisation. @@ -336,7 +380,7 @@ class Manager(object): """ if name not in self.orgs.keys(): taskd_cmd("add", "org", name) - neworg = Organisation(name) + neworg = Organisation(name, self.ignore_imperative) self._lazy_orgs[name] = neworg return neworg return None @@ -348,6 +392,8 @@ class Manager(object): """ org = self.get_org(name) if org is not None: + if self.ignore_imperative and is_imperative(name): + return for user in org.users.keys(): org.del_user(user) for group in org.groups.keys(): @@ -362,7 +408,7 @@ class Manager(object): def orgs(self): result = {} for org in os.listdir(mkpath()): - result[org] = Organisation(org) + result[org] = Organisation(org, self.ignore_imperative) return result @@ -452,6 +498,7 @@ def add_org(name): sys.exit(msg.format(name)) taskd_cmd("add", "org", name) + mark_imperative(name) @cli.command("del-org") @@ -485,6 +532,8 @@ def add_user(organisation, user): if userobj is None: msg = "User {} already exists in organisation {}." sys.exit(msg.format(user, organisation)) + else: + mark_imperative(organisation.name, "users", userobj.key) @cli.command("del-user") @@ -510,10 +559,12 @@ def add_group(organisation, group): """ Create a group for the given organisation. """ - userobj = organisation.add_group(group) - if userobj is None: + groupobj = organisation.add_group(group) + if groupobj is None: msg = "Group {} already exists in organisation {}." sys.exit(msg.format(group, organisation)) + else: + mark_imperative(organisation.name, "groups", groupobj.name) @cli.command("del-group") @@ -562,10 +613,12 @@ def process_json(json_file): """ data = json.load(json_file) - mgr = Manager() + mgr = Manager(ignore_imperative=True) add_or_delete(mgr.orgs.keys(), data.keys(), mgr.add_org, mgr.del_org) for org in mgr.orgs.values(): + if is_imperative(org.name): + continue add_or_delete(org.users.keys(), data[org.name]['users'], org.add_user, org.del_user) add_or_delete(org.groups.keys(), data[org.name]['groups'], diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 5d2e030a8f6d..79a7703f037e 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -41,7 +41,8 @@ import ./make-test.nix { for my $client ($client1, $client2) { $client->nest("initialize client for user $user", sub { $client->succeed( - su $user, "task rc.confirmation=no config confirmation no" + (su $user, "rm -rf /home/$user/.task"), + (su $user, "task rc.confirmation=no config confirmation no") ); my $exportinfo = $server->succeed( @@ -156,5 +157,12 @@ import ./make-test.nix { $client1->succeed(su "bar", "task add destroy even more >&2"); $client1->fail(su "bar", "task sync >&2"); }; + + readdImperativeUser; + + subtest "check whether declarative config overrides user bar", sub { + restartServer; + testSync "bar"; + }; ''; } From 9f1e536948ba2f7d87dc0919dc7f630f6723ab85 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 04:14:33 +0200 Subject: [PATCH 46/60] nixos/taskserver: Allow to specify expiration/bits At least this should allow for some customisation of how the certificates and keys are created. We now have two sub-namespaces within PKI so it should be more clear which options you have to set if you want to either manage your own CA or let the module create it automatically. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 66 +++++++++++++++---- .../services/misc/taskserver/helper-tool.py | 11 +++- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index dc73ad26eb6c..70e162904e98 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -17,7 +17,7 @@ let result = "${key} = ${mkVal val}"; in optionalString (val != null && val != []) result; - mkPkiOption = desc: mkOption { + mkManualPkiOption = desc: mkOption { type = types.nullOr types.path; default = null; description = desc + '' @@ -27,24 +27,60 @@ let ''; }; - pkiOptions = { - ca.cert = mkPkiOption '' + manualPkiOptions = { + ca.cert = mkManualPkiOption '' Fully qualified path to the CA certificate. ''; - server.cert = mkPkiOption '' + server.cert = mkManualPkiOption '' Fully qualified path to the server certificate. ''; - server.crl = mkPkiOption '' + server.crl = mkManualPkiOption '' Fully qualified path to the server certificate revocation list. ''; - server.key = mkPkiOption '' + server.key = mkManualPkiOption '' Fully qualified path to the server key. ''; }; + mkAutoDesc = preamble: '' + ${preamble} + + + This option is for the automatically handled CA and will be ignored if any + of the options are set. + + ''; + + mkExpireOption = desc: mkOption { + type = types.nullOr types.int; + default = null; + example = 365; + apply = val: if isNull val then -1 else val; + description = mkAutoDesc '' + The expiration time of ${desc} in days or null for no + expiration time. + ''; + }; + + autoPkiOptions = { + bits = mkOption { + type = types.int; + default = 4096; + example = 2048; + description = mkAutoDesc "The bit size for generated keys."; + }; + + expiration = { + ca = mkExpireOption "the CA certificate"; + server = mkExpireOption "the server certificate"; + client = mkExpireOption "client certificates"; + crl = mkExpireOption "the certificate revocation list (CRL)"; + }; + }; + needToCreateCA = let notFound = path: let dotted = concatStringsSep "." path; @@ -53,10 +89,10 @@ let mkSublist = key: val: let newPath = path ++ singleton key; in if isOption val - then attrByPath newPath (notFound newPath) cfg.pki + then attrByPath newPath (notFound newPath) cfg.pki.manual else findPkiDefinitions newPath val; in flatten (mapAttrsToList mkSublist attrs); - in all isNull (findPkiDefinitions [] pkiOptions); + in all isNull (findPkiDefinitions [] manualPkiOptions); configFile = pkgs.writeText "taskdrc" '' # systemd related @@ -130,6 +166,9 @@ let src = ./helper-tool.py; inherit taskd certtool; inherit (cfg) dataDir user group fqdn; + certBits = cfg.pki.auto.bits; + clientExpiration = cfg.pki.auto.expiration.client; + crlExpiration = cfg.pki.auto.expiration.crl; }}" > "$out/main.py" cat > "$out/setup.py" < Date: Tue, 12 Apr 2016 04:21:55 +0200 Subject: [PATCH 47/60] nixos/taskserver: Introduce an extraConfig option This is simply to add configuration lines to the generated configuration file. The reason why I didn't went for an attribute set is that the taskdrc file format doesn't map very well on Nix attributes, for example the following can be set in taskdrc: server = somestring server.key = anotherstring In order to use a Nix attribute set for that, it would be way too complicated, for example if we want to represent the mentioned example we'd have to do something like this: { server._top = somestring; server.key = anotherstring; } Of course, this would work as well but nothing is more simple than just appending raw strings. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 70e162904e98..d82e9f77ea6a 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -94,7 +94,7 @@ let in flatten (mapAttrsToList mkSublist attrs); in all isNull (findPkiDefinitions [] manualPkiOptions); - configFile = pkgs.writeText "taskdrc" '' + configFile = pkgs.writeText "taskdrc" ('' # systemd related daemon = false log = - @@ -130,7 +130,7 @@ let server.key = ${cfg.pki.server.key} server.crl = ${cfg.pki.server.crl} ''} - ''; + '' + cfg.extraConfig); orgOptions = { name, ... }: { options.users = mkOption { @@ -363,6 +363,15 @@ in { pki.manual = manualPkiOptions; pki.auto = autoPkiOptions; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = "client.cert = /tmp/debugging.cert"; + description = '' + Extra lines to append to the taskdrc configuration file. + ''; + }; }; }; From 2ced6fcc757806c772633424bb47b14ab700acbd Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 04:53:53 +0200 Subject: [PATCH 48/60] nixos/taskserver: Setup CA before main service We need to explicitly make sure the CA is created before we actually launch the main Taskserver service in order to avoid race conditions where the preStart phase of the main service could possibly corrupt certificates if it would be started in parallel. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index d82e9f77ea6a..c06287fe3b7e 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -396,6 +396,7 @@ in { systemd.services.taskserver-ca = mkIf needToCreateCA { requiredBy = [ "taskserver.service" ]; after = [ "taskserver-init.service" ]; + before = [ "taskserver.service" ]; description = "Initialize CA for TaskServer"; serviceConfig.Type = "oneshot"; serviceConfig.UMask = "0077"; From 5062bf1b841495f5aa69b76fae3054f75a169227 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 04:57:03 +0200 Subject: [PATCH 49/60] nixos/taskserver/helper: Assert CA existence We want to make sure that the helper tool won't work if the automatic CA wasn't properly set up. This not only avoids race conditions if the tool is started before the actual service is running but it also fails if something during CA setup has failed so the user can investigate what went wrong. Signed-off-by: aszlig --- .../services/misc/taskserver/helper-tool.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index abc7362cf7c5..e2c340fbd2a0 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -24,6 +24,10 @@ TASKD_USER = "@user@" TASKD_GROUP = "@group@" FQDN = "@fqdn@" +CA_KEY = os.path.join(TASKD_DATA_DIR, "keys", "ca.key") +CA_CERT = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert") +CRL_FILE = os.path.join(TASKD_DATA_DIR, "keys", "server.crl") + RE_CONFIGUSER = re.compile(r'^\s*user\s*=(.*)$') RE_USERKEY = re.compile(r'New user key: (.+)$', re.MULTILINE) @@ -151,8 +155,6 @@ def generate_key(org, user): privkey = os.path.join(basedir, "private.key") pubcert = os.path.join(basedir, "public.cert") - cakey = os.path.join(TASKD_DATA_DIR, "keys", "ca.key") - cacert = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert") try: os.makedirs(basedir, mode=0700) @@ -172,8 +174,8 @@ def generate_key(org, user): certtool_cmd( "-c", "--load-privkey", privkey, - "--load-ca-privkey", cakey, - "--load-ca-certificate", cacert, + "--load-ca-privkey", CA_KEY, + "--load-ca-certificate", CA_CERT, "--template", template, "--outfile", pubcert ) @@ -183,10 +185,6 @@ def generate_key(org, user): def revoke_key(org, user): - cakey = os.path.join(TASKD_DATA_DIR, "keys", "ca.key") - cacert = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert") - crl = os.path.join(TASKD_DATA_DIR, "keys", "server.crl") - basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user) if not os.path.exists(basedir): raise OSError("Keyfile directory for {} doesn't exist.".format(user)) @@ -197,16 +195,16 @@ def revoke_key(org, user): with create_template([expiration]) as template: oldcrl = NamedTemporaryFile(mode="wb", prefix="old-crl") - oldcrl.write(open(crl, "rb").read()) + oldcrl.write(open(CRL_FILE, "rb").read()) oldcrl.flush() certtool_cmd( "--generate-crl", "--load-crl", oldcrl.name, - "--load-ca-privkey", cakey, - "--load-ca-certificate", cacert, + "--load-ca-privkey", CA_KEY, + "--load-ca-certificate", CA_CERT, "--load-certificate", pubcert, "--template", template, - "--outfile", crl + "--outfile", CRL_FILE ) oldcrl.close() rmtree(basedir) @@ -432,11 +430,15 @@ ORGANISATION = OrganisationType() @click.group() -def cli(): +@click.pass_context +def cli(ctx): """ Manage Taskserver users and certificates """ - pass + for path in (CA_KEY, CA_CERT, CRL_FILE): + if not os.path.exists(path): + msg = "CA setup not done or incomplete, missing file {}." + ctx.fail(msg.format(path)) @cli.command("list-users") From 5be76d0b552ae5222cc8748baef2138c7acd91d4 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 05:07:52 +0200 Subject: [PATCH 50/60] nixos/taskserver: Reorder into one mkMerge No changes in functionality but rather just restructuring the module definitions to be one mkMerge, which now uses mkIf from the top-level scope of the CA initialization service so we can better abstract additional options we might need there. Signed-off-by: aszlig --- .../services/misc/taskserver/default.nix | 292 +++++++++--------- 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index c06287fe3b7e..520a9c2ee1e5 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -182,8 +182,6 @@ let propagatedBuildInputs = [ pkgs.pythonPackages.click ]; }; - withMeta = meta: defs: mkMerge [ defs { inherit meta; } ]; - in { options = { services.taskserver = { @@ -375,150 +373,152 @@ in { }; }; - config = withMeta { - doc = ./taskserver.xml; - } (mkIf cfg.enable { + config = mkMerge [ + (mkIf cfg.enable { + environment.systemPackages = [ pkgs.taskserver nixos-taskserver ]; - environment.systemPackages = [ pkgs.taskserver nixos-taskserver ]; - - users.users = optional (cfg.user == "taskd") { - name = "taskd"; - uid = config.ids.uids.taskd; - description = "Taskserver user"; - group = cfg.group; - }; - - users.groups = optional (cfg.group == "taskd") { - name = "taskd"; - gid = config.ids.gids.taskd; - }; - - systemd.services.taskserver-ca = mkIf needToCreateCA { - requiredBy = [ "taskserver.service" ]; - after = [ "taskserver-init.service" ]; - before = [ "taskserver.service" ]; - description = "Initialize CA for TaskServer"; - serviceConfig.Type = "oneshot"; - serviceConfig.UMask = "0077"; - - script = '' - silent_certtool() { - if ! output="$("${certtool}" "$@" 2>&1)"; then - echo "GNUTLS certtool invocation failed with output:" >&2 - echo "$output" >&2 - fi - } - - mkdir -m 0700 -p "${cfg.dataDir}/keys" - chown root:root "${cfg.dataDir}/keys" - - if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then - silent_certtool -p \ - --bits ${toString cfg.pki.auto.bits} \ - --outfile "${cfg.dataDir}/keys/ca.key" - silent_certtool -s \ - --template "${pkgs.writeText "taskserver-ca.template" '' - cn = ${cfg.fqdn} - expiration_days = ${toString cfg.pki.auto.expiration.ca} - cert_signing_key - ca - ''}" \ - --load-privkey "${cfg.dataDir}/keys/ca.key" \ - --outfile "${cfg.dataDir}/keys/ca.cert" - - chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert" - chmod g+r "${cfg.dataDir}/keys/ca.cert" - fi - - if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then - silent_certtool -p \ - --bits ${toString cfg.pki.auto.bits} \ - --outfile "${cfg.dataDir}/keys/server.key" - - silent_certtool -c \ - --template "${pkgs.writeText "taskserver-cert.template" '' - cn = ${cfg.fqdn} - expiration_days = ${toString cfg.pki.auto.expiration.server} - tls_www_server - encryption_key - signing_key - ''}" \ - --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ - --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ - --load-privkey "${cfg.dataDir}/keys/server.key" \ - --outfile "${cfg.dataDir}/keys/server.cert" - - chgrp "${cfg.group}" \ - "${cfg.dataDir}/keys/server.key" \ - "${cfg.dataDir}/keys/server.cert" - - chmod g+r \ - "${cfg.dataDir}/keys/server.key" \ - "${cfg.dataDir}/keys/server.cert" - fi - - if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then - silent_certtool --generate-crl \ - --template "${pkgs.writeText "taskserver-crl.template" '' - expiration_days = ${toString cfg.pki.auto.expiration.crl} - ''}" \ - --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ - --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ - --outfile "${cfg.dataDir}/keys/server.crl" - - chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl" - chmod g+r "${cfg.dataDir}/keys/server.crl" - fi - - chmod go+x "${cfg.dataDir}/keys" - ''; - }; - - systemd.services.taskserver-init = { - requiredBy = [ "taskserver.service" ]; - description = "Initialize Taskserver Data Directory"; - - preStart = '' - mkdir -m 0770 -p "${cfg.dataDir}" - chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}" - ''; - - script = '' - ${taskd} init - echo "include ${configFile}" > "${cfg.dataDir}/config" - touch "${cfg.dataDir}/.is_initialized" - ''; - - environment.TASKDDATA = cfg.dataDir; - - unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized"; - - serviceConfig.Type = "oneshot"; - serviceConfig.User = cfg.user; - serviceConfig.Group = cfg.group; - serviceConfig.PermissionsStartOnly = true; - }; - - systemd.services.taskserver = { - description = "Taskwarrior Server"; - - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - - environment.TASKDDATA = cfg.dataDir; - - preStart = let - jsonOrgs = builtins.toJSON cfg.organisations; - jsonFile = pkgs.writeText "orgs.json" jsonOrgs; - in "${nixos-taskserver}/bin/nixos-taskserver process-json '${jsonFile}'"; - - serviceConfig = { - ExecStart = "@${taskd} taskd server"; - ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; - PermissionsStartOnly = true; - User = cfg.user; - Group = cfg.group; + users.users = optional (cfg.user == "taskd") { + name = "taskd"; + uid = config.ids.uids.taskd; + description = "Taskserver user"; + group = cfg.group; }; - }; - }); + + users.groups = optional (cfg.group == "taskd") { + name = "taskd"; + gid = config.ids.gids.taskd; + }; + + systemd.services.taskserver-init = { + requiredBy = [ "taskserver.service" ]; + description = "Initialize Taskserver Data Directory"; + + preStart = '' + mkdir -m 0770 -p "${cfg.dataDir}" + chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}" + ''; + + script = '' + ${taskd} init + echo "include ${configFile}" > "${cfg.dataDir}/config" + touch "${cfg.dataDir}/.is_initialized" + ''; + + environment.TASKDDATA = cfg.dataDir; + + unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized"; + + serviceConfig.Type = "oneshot"; + serviceConfig.User = cfg.user; + serviceConfig.Group = cfg.group; + serviceConfig.PermissionsStartOnly = true; + }; + + systemd.services.taskserver = { + description = "Taskwarrior Server"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + environment.TASKDDATA = cfg.dataDir; + + preStart = let + jsonOrgs = builtins.toJSON cfg.organisations; + jsonFile = pkgs.writeText "orgs.json" jsonOrgs; + helperTool = "${nixos-taskserver}/bin/nixos-taskserver"; + in "${helperTool} process-json '${jsonFile}'"; + + serviceConfig = { + ExecStart = "@${taskd} taskd server"; + ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; + PermissionsStartOnly = true; + User = cfg.user; + Group = cfg.group; + }; + }; + }) + (mkIf needToCreateCA { + systemd.services.taskserver-ca = { + requiredBy = [ "taskserver.service" ]; + after = [ "taskserver-init.service" ]; + before = [ "taskserver.service" ]; + description = "Initialize CA for TaskServer"; + serviceConfig.Type = "oneshot"; + serviceConfig.UMask = "0077"; + + script = '' + silent_certtool() { + if ! output="$("${certtool}" "$@" 2>&1)"; then + echo "GNUTLS certtool invocation failed with output:" >&2 + echo "$output" >&2 + fi + } + + mkdir -m 0700 -p "${cfg.dataDir}/keys" + chown root:root "${cfg.dataDir}/keys" + + if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then + silent_certtool -p \ + --bits ${toString cfg.pki.auto.bits} \ + --outfile "${cfg.dataDir}/keys/ca.key" + silent_certtool -s \ + --template "${pkgs.writeText "taskserver-ca.template" '' + cn = ${cfg.fqdn} + expiration_days = ${toString cfg.pki.auto.expiration.ca} + cert_signing_key + ca + ''}" \ + --load-privkey "${cfg.dataDir}/keys/ca.key" \ + --outfile "${cfg.dataDir}/keys/ca.cert" + + chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert" + chmod g+r "${cfg.dataDir}/keys/ca.cert" + fi + + if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then + silent_certtool -p \ + --bits ${toString cfg.pki.auto.bits} \ + --outfile "${cfg.dataDir}/keys/server.key" + + silent_certtool -c \ + --template "${pkgs.writeText "taskserver-cert.template" '' + cn = ${cfg.fqdn} + expiration_days = ${toString cfg.pki.auto.expiration.server} + tls_www_server + encryption_key + signing_key + ''}" \ + --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ + --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ + --load-privkey "${cfg.dataDir}/keys/server.key" \ + --outfile "${cfg.dataDir}/keys/server.cert" + + chgrp "${cfg.group}" \ + "${cfg.dataDir}/keys/server.key" \ + "${cfg.dataDir}/keys/server.cert" + + chmod g+r \ + "${cfg.dataDir}/keys/server.key" \ + "${cfg.dataDir}/keys/server.cert" + fi + + if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then + silent_certtool --generate-crl \ + --template "${pkgs.writeText "taskserver-crl.template" '' + expiration_days = ${toString cfg.pki.auto.expiration.crl} + ''}" \ + --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \ + --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \ + --outfile "${cfg.dataDir}/keys/server.crl" + + chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl" + chmod g+r "${cfg.dataDir}/keys/server.crl" + fi + + chmod go+x "${cfg.dataDir}/keys" + ''; + }; + }) + { meta.doc = ./taskserver.xml; } + ]; } From ce0954020c71007b7a9ec2822949d31f18aea170 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 05:13:04 +0200 Subject: [PATCH 51/60] nixos/taskserver: Set allowedTCPPorts accordingly As suggested by @matthiasbeyer: "We might add a short note that this port has to be opened in the firewall, or is this done by the service automatically?" This commit now adds the listenPort to networking.firewall.allowedTCPPorts as soon as the listenHost is not "localhost". In addition to that, this is now also documented in the listenHost option declaration and I have removed disabling of the firewall from the VM test. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 8 ++++++++ nixos/tests/taskserver.nix | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 520a9c2ee1e5..8054dbe9f662 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -324,8 +324,13 @@ in { listenHost = mkOption { type = types.str; default = "localhost"; + example = "::"; description = '' The address (IPv4, IPv6 or DNS) to listen on. + + If the value is something else than localhost the + port defined by is automatically added to + . ''; }; @@ -519,6 +524,9 @@ in { ''; }; }) + (mkIf (cfg.listenHost != "localhost") { + networking.firewall.allowedTCPPorts = [ cfg.listenPort ]; + }) { meta.doc = ./taskserver.xml; } ]; } diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 79a7703f037e..0521f97431b3 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -3,7 +3,6 @@ import ./make-test.nix { nodes = rec { server = { - networking.firewall.enable = false; services.taskserver.enable = true; services.taskserver.listenHost = "::"; services.taskserver.fqdn = "server"; @@ -14,7 +13,6 @@ import ./make-test.nix { }; client1 = { pkgs, ... }: { - networking.firewall.enable = false; environment.systemPackages = [ pkgs.taskwarrior pkgs.gnutls ]; users.users.alice.isNormalUser = true; users.users.bob.isNormalUser = true; From e2383b84f88e0e7d35f6a3a846b54c69e3bee6ee Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 05:38:37 +0200 Subject: [PATCH 52/60] nixos/taskserver/helper: Improve CLI subcommands Try to match the subcommands to act more like the subcommands from the taskd binary and also add a subcommand to list groups. Signed-off-by: aszlig --- .../services/misc/taskserver/helper-tool.py | 55 +++++++++++++++---- nixos/tests/taskserver.nix | 20 +++---- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index e2c340fbd2a0..f5d3c2ecbd37 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -441,7 +441,31 @@ def cli(ctx): ctx.fail(msg.format(path)) -@cli.command("list-users") +@cli.group("org") +def org_cli(): + """ + Manage organisations + """ + pass + + +@cli.group("user") +def user_cli(): + """ + Manage users + """ + pass + + +@cli.group("group") +def group_cli(): + """ + Manage groups + """ + pass + + +@user_cli.command("list") @click.argument("organisation", type=ORGANISATION) def list_users(organisation): """ @@ -452,7 +476,18 @@ def list_users(organisation): sys.stdout.write(user.name + "\n") -@cli.command("list-orgs") +@group_cli.command("list") +@click.argument("organisation", type=ORGANISATION) +def list_groups(organisation): + """ + List all users belonging to the specified organisation. + """ + label("The following users exists for {}:".format(organisation.name)) + for group in organisation.groups.values(): + sys.stdout.write(group.name + "\n") + + +@org_cli.command("list") def list_orgs(): """ List available organisations @@ -462,7 +497,7 @@ def list_orgs(): sys.stdout.write(org.name + "\n") -@cli.command("get-uuid") +@user_cli.command("getkey") @click.argument("organisation", type=ORGANISATION) @click.argument("user") def get_uuid(organisation, user): @@ -478,7 +513,7 @@ def get_uuid(organisation, user): sys.stdout.write(user.key + "\n") -@cli.command("export-user") +@user_cli.command("export") @click.argument("organisation", type=ORGANISATION) @click.argument("user") def export_user(organisation, user): @@ -496,7 +531,7 @@ def export_user(organisation, user): sys.stdout.write(userobj.export()) -@cli.command("add-org") +@org_cli.command("add") @click.argument("name") def add_org(name): """ @@ -510,7 +545,7 @@ def add_org(name): mark_imperative(name) -@cli.command("del-org") +@org_cli.command("remove") @click.argument("name") def del_org(name): """ @@ -526,7 +561,7 @@ def del_org(name): click.echo(msg.format(name), err=True) -@cli.command("add-user") +@user_cli.command("add") @click.argument("organisation", type=ORGANISATION) @click.argument("user") def add_user(organisation, user): @@ -545,7 +580,7 @@ def add_user(organisation, user): mark_imperative(organisation.name, "users", userobj.key) -@cli.command("del-user") +@user_cli.command("remove") @click.argument("organisation", type=ORGANISATION) @click.argument("user") def del_user(organisation, user): @@ -561,7 +596,7 @@ def del_user(organisation, user): click.echo(msg.format(user), err=True) -@cli.command("add-group") +@group_cli.command("add") @click.argument("organisation", type=ORGANISATION) @click.argument("group") def add_group(organisation, group): @@ -576,7 +611,7 @@ def add_group(organisation, group): mark_imperative(organisation.name, "groups", groupobj.name) -@cli.command("del-group") +@group_cli.command("remove") @click.argument("organisation", type=ORGANISATION) @click.argument("group") def del_group(organisation, group): diff --git a/nixos/tests/taskserver.nix b/nixos/tests/taskserver.nix index 0521f97431b3..d770b20a7757 100644 --- a/nixos/tests/taskserver.nix +++ b/nixos/tests/taskserver.nix @@ -44,7 +44,7 @@ import ./make-test.nix { ); my $exportinfo = $server->succeed( - "nixos-taskserver export-user $org $user" + "nixos-taskserver user export $org $user" ); $exportinfo =~ s/'/'\\'''/g; @@ -74,10 +74,10 @@ import ./make-test.nix { sub readdImperativeUser { $server->nest("(re-)add imperative user bar", sub { - $server->execute("nixos-taskserver del-org imperativeOrg"); + $server->execute("nixos-taskserver org remove imperativeOrg"); $server->succeed( - "nixos-taskserver add-org imperativeOrg", - "nixos-taskserver add-user imperativeOrg bar" + "nixos-taskserver org add imperativeOrg", + "nixos-taskserver user add imperativeOrg bar" ); setupClientsFor "imperativeOrg", "bar"; }); @@ -109,9 +109,9 @@ import ./make-test.nix { $server->waitForUnit("taskserver.service"); $server->succeed( - "nixos-taskserver list-users testOrganisation | grep -qxF alice", - "nixos-taskserver list-users testOrganisation | grep -qxF foo", - "nixos-taskserver list-users anotherOrganisation | grep -qxF bob" + "nixos-taskserver user list testOrganisation | grep -qxF alice", + "nixos-taskserver user list testOrganisation | grep -qxF foo", + "nixos-taskserver user list anotherOrganisation | grep -qxF bob" ); $server->waitForOpenPort(${portStr}); @@ -125,7 +125,7 @@ import ./make-test.nix { testSync $_ for ("alice", "bob", "foo"); - $server->fail("nixos-taskserver add-user imperativeOrg bar"); + $server->fail("nixos-taskserver user add imperativeOrg bar"); readdImperativeUser; testSync "bar"; @@ -133,7 +133,7 @@ import ./make-test.nix { subtest "checking certificate revocation of user bar", sub { $client1->succeed(checkClientCert "bar"); - $server->succeed("nixos-taskserver del-user imperativeOrg bar"); + $server->succeed("nixos-taskserver user remove imperativeOrg bar"); restartServer; $client1->fail(checkClientCert "bar"); @@ -147,7 +147,7 @@ import ./make-test.nix { subtest "checking certificate revocation of org imperativeOrg", sub { $client1->succeed(checkClientCert "bar"); - $server->succeed("nixos-taskserver del-org imperativeOrg"); + $server->succeed("nixos-taskserver org remove imperativeOrg"); restartServer; $client1->fail(checkClientCert "bar"); From dd0d64afea9f184e4408016ed1413e2284cc67a2 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 06:19:59 +0200 Subject: [PATCH 53/60] nixos/taskserver: Finish module documentation Apart from the options manual, this should cover the basics for setting up a Taskserver. I am not a native speaker so this can and (probably) should be improved, especially the wording/grammar. Signed-off-by: aszlig --- .../modules/services/misc/taskserver/doc.xml | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml index b41747872c66..15125b1411bd 100644 --- a/nixos/modules/services/misc/taskserver/doc.xml +++ b/nixos/modules/services/misc/taskserver/doc.xml @@ -32,7 +32,7 @@ So in order to make it easier to handle your own CA, there is a helper tool called nixos-taskserver which manages the custom - CA along with Taskserver users and groups. + CA along with Taskserver organisations, users and groups. @@ -46,7 +46,99 @@ along with the UUID of the user, so it handles all of the credentials needed in order to setup the Taskwarrior client to work with a Taskserver. + - +
+ The nixos-taskserver tool + + + Because Taskserver by default only provides scripts to setup users + imperatively, the nixos-taskserver tool is used for + addition and deletion of organisations along with users and groups defined + by and as well for + imperative set up. + + + + The tool is designed to not interfere if the command is used to manually + set up some organisations, users or groups. + + + + For example if you add a new organisation using + nixos-taskserver org add foo, the organisation is not + modified and deleted no matter what you define in + , even if you're adding + the same organisation in that option. + + + + The tool is modelled to imitate the official taskd + command, documentation for each subcommand can be shown by using the + switch. + +
+
+ Declarative/automatic CA management + + + Everything is done according to what you specify in the module options, + however in order to set up a Taskwarrior client for synchronisation with a + Taskserver instance, you have to transfer the keys and certificates to the + client machine. + + + + This is done using + nixos-taskserver user export $orgname $username which + is printing a shell script fragment to stdout which can either be used + verbatim or adjusted to import the user on the client machine. + + + + For example, let's say you have the following configuration: + +{ + services.taskserver.enable = true; + services.taskserver.fqdn = "server"; + services.taskserver.listenHost = "::"; + services.taskserver.organisations.NixOS.users = [ "alice" ]; +} + + This creates an organisation called NixOS with the user + alice. + + + + Now in order to import the alice user to another + machine alicebox, all we need to do is something like + this: + +$ ssh server nixos-taskserver user export NixOS alice | sh + + Of course, if no SSH daemon is available on the server you can also copy + & paste it directly into a shell. + + + + After this step the user should be set up and you can start synchronising + your tasks for the first time with task sync init on + alicebox. + + + + Subsequent synchronisation requests merely require the command + task sync after that stage. + +
+
+ Manual CA management + + + If you set any options within + , the automatic user and + CA management by the nixos-taskserver is disabled and + you need to create certificates and keys by yourself. +
From bb7a8197351e151d1e7918fe2c54de705fa65cc8 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 06:26:39 +0200 Subject: [PATCH 54/60] nixos/taskserver: Set up service namespaces The Taskserver doesn't need access to the full /dev nor does it need a shared /tmp. In addition, the initialisation services don't need network access, so let's constrain them to the loopback device. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 8054dbe9f662..e0e94dac48f1 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -417,6 +417,9 @@ in { serviceConfig.User = cfg.user; serviceConfig.Group = cfg.group; serviceConfig.PermissionsStartOnly = true; + serviceConfig.PrivateNetwork = true; + serviceConfig.PrivateDevices = true; + serviceConfig.PrivateTmp = true; }; systemd.services.taskserver = { @@ -437,6 +440,8 @@ in { ExecStart = "@${taskd} taskd server"; ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; PermissionsStartOnly = true; + PrivateTmp = true; + PrivateDevices = true; User = cfg.user; Group = cfg.group; }; @@ -450,6 +455,8 @@ in { description = "Initialize CA for TaskServer"; serviceConfig.Type = "oneshot"; serviceConfig.UMask = "0077"; + serviceConfig.PrivateNetwork = true; + serviceConfig.PrivateTmp = true; script = '' silent_certtool() { From cf46256bbbf559f8cc0c8c9c8a6dd7fdb0af82fd Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 06:33:04 +0200 Subject: [PATCH 55/60] nixos/taskserver: Improve service dependencies Using requiredBy is a bad idea for the initialisation units, because whenever the Taskserver service is restarted the initialisation units get restarted as well. Also, make sure taskserver-init.service will be ordered *before* taskserver.service. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index e0e94dac48f1..261d4d4d4b0d 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -395,7 +395,8 @@ in { }; systemd.services.taskserver-init = { - requiredBy = [ "taskserver.service" ]; + wantedBy = [ "taskserver.service" ]; + before = [ "taskserver.service" ]; description = "Initialize Taskserver Data Directory"; preStart = '' @@ -449,7 +450,7 @@ in { }) (mkIf needToCreateCA { systemd.services.taskserver-ca = { - requiredBy = [ "taskserver.service" ]; + wantedBy = [ "taskserver.service" ]; after = [ "taskserver-init.service" ]; before = [ "taskserver.service" ]; description = "Initialize CA for TaskServer"; From 980f557c460c32bcdeb10100e6ddf9fd799a6059 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 06:43:21 +0200 Subject: [PATCH 56/60] nixos/taskserver: Restart service on failure This is the recommended way for long-running services and ensures that Taskserver will keep running until it has been stopped manually. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 261d4d4d4b0d..4d3afdeedfa7 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -440,6 +440,7 @@ in { serviceConfig = { ExecStart = "@${taskd} taskd server"; ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID"; + Restart = "on-failure"; PermissionsStartOnly = true; PrivateTmp = true; PrivateDevices = true; From e06dd999f7858c7a924cec552bcd5ac90193e00d Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 07:03:19 +0200 Subject: [PATCH 57/60] nixos/taskserver: Fix wrong option doc references The options client.allow and client.deny are gone since the commit 8b793d1916387c67f8eeb137789b1b41a1f94537, so let's fix that. No feature changes, only fixes the descriptions of allowedClientIDs and disallowedClientIDs. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 4d3afdeedfa7..0b86b42f2cc7 100644 --- a/nixos/modules/services/misc/taskserver/default.nix +++ b/nixos/modules/services/misc/taskserver/default.nix @@ -303,7 +303,7 @@ in { The values all or none have special meaning. Overidden by any entry in the option - . + . ''; }; @@ -316,8 +316,8 @@ in { client id (such as task 2.3.0). The values all or none have - special meaning. Any entry here overrides these in - . + special meaning. Any entry here overrides those in + . ''; }; From 394e64e4fbc7ff4f77fd241d5811acf2bdd5a998 Mon Sep 17 00:00:00 2001 From: aszlig Date: Tue, 12 Apr 2016 07:13:43 +0200 Subject: [PATCH 58/60] nixos/taskserver/helper: Fix docstring of add_user We have already revamped the CLI subcommands in commit e2383b84f88e0e7d35f6a3a846b54c69e3bee6ee. This was just an artifact that was left because of this. Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/helper-tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index f5d3c2ecbd37..03e7cdf8987a 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -570,7 +570,7 @@ def add_user(organisation, user): and print the key of the new user. The client certificate along with it's public key can be shown via the - 'export-user' subcommand. + 'user export' subcommand. """ userobj = organisation.add_user(user) if userobj is None: From 940120a711930eab13883f56c677cf2a4580b14e Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 14 Apr 2016 21:16:14 +0200 Subject: [PATCH 59/60] nixos/taskserver/doc: Improve example org name Suggested by @nbp: "Choose a better organization name in this example, such that it is less confusing. Maybe something like my-company" Signed-off-by: aszlig --- nixos/modules/services/misc/taskserver/doc.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml index 15125b1411bd..48591129264a 100644 --- a/nixos/modules/services/misc/taskserver/doc.xml +++ b/nixos/modules/services/misc/taskserver/doc.xml @@ -102,11 +102,11 @@ services.taskserver.enable = true; services.taskserver.fqdn = "server"; services.taskserver.listenHost = "::"; - services.taskserver.organisations.NixOS.users = [ "alice" ]; + services.taskserver.organisations.my-company.users = [ "alice" ]; } - This creates an organisation called NixOS with the user - alice. + This creates an organisation called my-company with the + user alice. @@ -114,7 +114,7 @@ machine alicebox, all we need to do is something like this: -$ ssh server nixos-taskserver user export NixOS alice | sh +$ ssh server nixos-taskserver user export my-company alice | sh Of course, if no SSH daemon is available on the server you can also copy & paste it directly into a shell. From c36d6e59647794aa1c4dbb2dcd7e5242d9c5b6b8 Mon Sep 17 00:00:00 2001 From: aszlig Date: Thu, 14 Apr 2016 21:31:02 +0200 Subject: [PATCH 60/60] nixos/doc: Revert allowing olinks from options This reverts commit 1d77dcaed37ab47bfe2d90711c01b475a514ff25. It will be reintroduced along with #14700 as a separate branch, as suggested by @nbp. I added this to this branch because I thought it was a necessary dependency, but it turns out that the build of the manual/manpages still succeeds and merely prints a warning like this: warning: failed to load external entity "olinkdb.xml" Olink error: could not open target database 'olinkdb.xml'. Error: unresolved olink: targetdoc/targetptr = 'manual/module-taskserver'. The olink itself will be replaced by "???", so users looking at the description of the option in question will still see the reference to the NixOS manual, like this: More instructions about NixOS in conjuction with Taskserver can be found in the NixOS manual at ???. Signed-off-by: aszlig --- nixos/doc/manual/default.nix | 75 +++++++----------------------------- 1 file changed, 13 insertions(+), 62 deletions(-) diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix index 6261f83ac8b2..130b016aa266 100644 --- a/nixos/doc/manual/default.nix +++ b/nixos/doc/manual/default.nix @@ -73,63 +73,6 @@ let ''; - manualXsltprocOptions = toString [ - "--param section.autolabel 1" - "--param section.label.includes.component.label 1" - "--stringparam html.stylesheet style.css" - "--param xref.with.number.and.title 1" - "--param toc.section.depth 3" - "--stringparam admon.style ''" - "--stringparam callout.graphics.extension .gif" - "--stringparam current.docid manual" - "--param chunk.section.depth 0" - "--param chunk.first.sections 1" - "--param use.id.as.filename 1" - "--stringparam generate.toc 'book toc appendix toc'" - "--stringparam chunk.toc ${toc}" - ]; - - olinkDB = stdenv.mkDerivation { - name = "manual-olinkdb"; - - inherit sources; - - buildInputs = [ libxml2 libxslt ]; - - buildCommand = '' - ${copySources} - - xsltproc \ - ${manualXsltprocOptions} \ - --stringparam collect.xref.targets only \ - --stringparam targets.filename "$out/manual.db" \ - --nonet --xinclude \ - ${docbook5_xsl}/xml/xsl/docbook/xhtml/chunktoc.xsl \ - ./manual.xml - - # Check the validity of the man pages sources. - xmllint --noout --nonet --xinclude --noxincludenode \ - --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \ - ./man-pages.xml - - cat > "$out/olinkdb.xml" < - - ]> - - - Allows for cross-referencing olinks between the manpages - and the HTML/PDF manuals. - - - &manualtargets; - - EOF - ''; - }; - in rec { # The NixOS options in JSON format. @@ -172,8 +115,18 @@ in rec { dst=$out/share/doc/nixos mkdir -p $dst xsltproc \ - ${manualXsltprocOptions} \ - --stringparam target.database.document "${olinkDB}/olinkdb.xml" \ + --param section.autolabel 1 \ + --param section.label.includes.component.label 1 \ + --stringparam html.stylesheet style.css \ + --param xref.with.number.and.title 1 \ + --param toc.section.depth 3 \ + --stringparam admon.style "" \ + --stringparam callout.graphics.extension .gif \ + --param chunk.section.depth 0 \ + --param chunk.first.sections 1 \ + --param use.id.as.filename 1 \ + --stringparam generate.toc "book toc appendix toc" \ + --stringparam chunk.toc ${toc} \ --nonet --xinclude --output $dst/ \ ${docbook5_xsl}/xml/xsl/docbook/xhtml/chunktoc.xsl ./manual.xml @@ -205,7 +158,6 @@ in rec { dst=$out/share/doc/nixos mkdir -p $dst xmllint --xinclude manual.xml | dblatex -o $dst/manual.pdf - \ - -P target.database.document="${olinkDB}/olinkdb.xml" \ -P doc.collab.show=0 \ -P latex.output.revhistory=0 @@ -225,7 +177,7 @@ in rec { buildCommand = '' ${copySources} - # Check the validity of the man pages sources. + # Check the validity of the manual sources. xmllint --noout --nonet --xinclude --noxincludenode \ --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \ ./man-pages.xml @@ -237,7 +189,6 @@ in rec { --param man.output.base.dir "'$out/share/man/'" \ --param man.endnotes.are.numbered 0 \ --param man.break.after.slash 1 \ - --stringparam target.database.document "${olinkDB}/olinkdb.xml" \ ${docbook5_xsl}/xml/xsl/docbook/manpages/docbook.xsl \ ./man-pages.xml '';