nixpkgs/nixos/modules/services/misc/taskserver/default.nix
aszlig 9279ec732b
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 <aszlig@redmoonstudios.org>
2016-04-12 04:21:55 +02:00

524 lines
15 KiB
Nix

{ config, lib, pkgs, ... }:
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;
mkManualPkiOption = desc: mkOption {
type = types.nullOr types.path;
default = null;
description = desc + ''
<note><para>
Setting this option will prevent automatic CA creation and handling.
</para></note>
'';
};
manualPkiOptions = {
ca.cert = mkManualPkiOption ''
Fully qualified path to the CA certificate.
'';
server.cert = mkManualPkiOption ''
Fully qualified path to the server certificate.
'';
server.crl = mkManualPkiOption ''
Fully qualified path to the server certificate revocation list.
'';
server.key = mkManualPkiOption ''
Fully qualified path to the server key.
'';
};
mkAutoDesc = preamble: ''
${preamble}
<note><para>
This option is for the automatically handled CA and will be ignored if any
of the <option>services.taskserver.pki.manual.*</option> options are set.
</para></note>
'';
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 <literal>null</literal> 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;
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.manual
else findPkiDefinitions newPath val;
in flatten (mapAttrsToList mkSublist attrs);
in all isNull (findPkiDefinitions [] manualPkiOptions);
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.allow" cfg.allowedClientIDs}
${mkConfLine "client.deny" cfg.disallowedClientIDs}
# server
server = ${cfg.listenHost}:${toString cfg.listenPort}
${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}
''}
'' + cfg.extraConfig);
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.
'';
};
};
mkShellStr = val: "'${replaceStrings ["'"] ["'\\''"] val}'";
certtool = "${pkgs.gnutls}/bin/certtool";
nixos-taskserver = pkgs.buildPythonPackage {
name = "nixos-taskserver";
namePrefix = "";
src = pkgs.runCommand "nixos-taskserver-src" {} ''
mkdir -p "$out"
cat "${pkgs.substituteAll {
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" <<EOF
from setuptools import setup
setup(name="nixos-taskserver",
py_modules=["main"],
install_requires=["Click"],
entry_points="[console_scripts]\\nnixos-taskserver=main:cli")
EOF
'';
propagatedBuildInputs = [ pkgs.pythonPackages.click ];
};
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.
More instructions about NixOS in conjuction with Taskserver can be
found in the NixOS manual at
<olink targetdoc="manual" targetptr="module-taskserver"/>.
'';
};
user = mkOption {
type = types.str;
default = "taskd";
description = "User for Taskserver.";
};
group = mkOption {
type = types.str;
default = "taskd";
description = "Group for Taskserver.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/taskserver";
description = "Data directory for Taskserver.";
};
ciphers = mkOption {
type = types.nullOr (types.separatedString ":");
default = null;
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 <link xlink:href="${url}"/> for full details.
'';
};
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 <option>users</option> and
<option>groups</option>.
'';
};
confirmation = mkOption {
type = types.bool;
default = true;
description = ''
Determines whether certain commands are confirmed.
'';
};
debug = mkOption {
type = types.bool;
default = false;
description = ''
Logs debugging information.
'';
};
extensions = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Fully qualified path of the Taskserver extension scripts.
Currently there are none.
'';
};
ipLog = mkOption {
type = types.bool;
default = false;
description = ''
Logs the IP addresses of incoming requests.
'';
};
queueSize = mkOption {
type = types.int;
default = 10;
description = ''
Size of the connection backlog, see <citerefentry>
<refentrytitle>listen</refentrytitle>
<manvolnum>2</manvolnum>
</citerefentry>.
'';
};
requestLimit = mkOption {
type = types.int;
default = 1048576;
description = ''
Size limit of incoming requests, in bytes.
'';
};
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 <literal>task 2.3.0</literal>).
The values <literal>all</literal> or <literal>none</literal> have
special meaning. Overidden by any entry in the option
<option>services.taskserver.client.deny</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 <literal>task 2.3.0</literal>).
The values <literal>all</literal> or <literal>none</literal> have
special meaning. Any entry here overrides these in
<option>services.taskserver.client.allow</option>.
'';
};
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.
'';
};
fqdn = mkOption {
type = types.str;
default = "localhost";
description = ''
The fully qualified domain name of this server, which is also used
as the common name in the certificates.
'';
};
trust = mkOption {
type = types.enum [ "allow all" "strict" ];
default = "strict";
description = ''
Determines how client certificates are validated.
The value <literal>allow all</literal> performs no client
certificate validation. This is not recommended. The value
<literal>strict</literal> causes the client certificate to be
validated against a CA.
'';
};
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.
'';
};
};
};
config = withMeta {
doc = ./taskserver.xml;
} (mkIf cfg.enable {
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" ];
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;
};
};
});
}