depot/nixos/modules/services/misc/taskserver/default.nix
Luke Granger-Brown 57725ef3ec Squashed 'third_party/nixpkgs/' content from commit 76612b17c0ce
git-subtree-dir: third_party/nixpkgs
git-subtree-split: 76612b17c0ce71689921ca12d9ffdc9c23ce40b2
2024-11-10 23:59:47 +00:00

567 lines
18 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.services.taskserver;
taskd = "${pkgs.taskserver}/bin/taskd";
mkManualPkiOption = desc: lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
${desc}
::: {.note}
Setting this option will prevent automatic CA creation and handling.
:::
'';
};
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}
This option is for the automatically handled CA and will be ignored if any
of the {option}`services.taskserver.pki.manual.*` options are set.
:::
'';
mkExpireOption = desc: lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 365;
apply = val: if val == null then -1 else val;
description = mkAutoDesc ''
The expiration time of ${desc} in days or `null` for no
expiration time.
'';
};
autoPkiOptions = {
bits = lib.mkOption {
type = lib.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 = lib.concatStringsSep "." path;
in throw "Can't find option definitions for path `${dotted}'.";
findPkiDefinitions = path: attrs: let
mkSublist = key: val: let
newPath = path ++ lib.singleton key;
in if lib.isOption val
then lib.attrByPath newPath (notFound newPath) cfg.pki.manual
else findPkiDefinitions newPath val;
in lib.flatten (lib.mapAttrsToList mkSublist attrs);
in lib.all (x: x == null) (findPkiDefinitions [] manualPkiOptions);
orgOptions = { ... }: {
options.users = lib.mkOption {
type = lib.types.uniq (lib.types.listOf lib.types.str);
default = [];
example = [ "alice" "bob" ];
description = ''
A list of user names that belong to the organization.
'';
};
options.groups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
example = [ "workers" "slackers" ];
description = ''
A list of group names that belong to the organization.
'';
};
};
certtool = "${pkgs.gnutls.bin}/bin/certtool";
nixos-taskserver = with pkgs.python3.pkgs; buildPythonApplication {
name = "nixos-taskserver";
src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } ''
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;
isAutoConfig = if needToCreateCA then "True" else "False";
}}" > "$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 = [ click ];
};
in {
options = {
services.taskserver = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = let
url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver";
in ''
Whether to enable the Taskwarrior 2 server.
More instructions about NixOS in conjunction with Taskserver can be
found [in the NixOS manual](${url}).
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "taskd";
description = "User for Taskserver.";
};
group = lib.mkOption {
type = lib.types.str;
default = "taskd";
description = "Group for Taskserver.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/taskserver";
description = "Data directory for Taskserver.";
};
ciphers = lib.mkOption {
type = lib.types.nullOr (lib.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 <${url}> for full details.
'';
};
organisations = lib.mkOption {
type = lib.types.attrsOf (lib.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` and
{option}`groups`.
'';
};
confirmation = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Determines whether certain commands are confirmed.
'';
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Logs debugging information.
'';
};
extensions = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Fully qualified path of the Taskserver extension scripts.
Currently there are none.
'';
};
ipLog = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Logs the IP addresses of incoming requests.
'';
};
queueSize = lib.mkOption {
type = lib.types.int;
default = 10;
description = ''
Size of the connection backlog, see {manpage}`listen(2)`.
'';
};
requestLimit = lib.mkOption {
type = lib.types.int;
default = 1048576;
description = ''
Size limit of incoming requests, in bytes.
'';
};
allowedClientIDs = lib.mkOption {
type = with lib.types; either str (listOf 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. Overridden by any entry in the option
{option}`services.taskserver.disallowedClientIDs`.
'';
};
disallowedClientIDs = lib.mkOption {
type = with lib.types; either str (listOf 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 those in
{option}`services.taskserver.allowedClientIDs`.
'';
};
listenHost = lib.mkOption {
type = lib.types.str;
default = "localhost";
example = "::";
description = ''
The address (IPv4, IPv6 or DNS) to listen on.
'';
};
listenPort = lib.mkOption {
type = lib.types.int;
default = 53589;
description = ''
Port number of the Taskserver.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the firewall for the specified Taskserver port.
'';
};
fqdn = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.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.manual = manualPkiOptions;
pki.auto = autoPkiOptions;
config = lib.mkOption {
type = lib.types.attrs;
example.client.cert = "/tmp/debugging.cert";
description = ''
Configuration options to pass to Taskserver.
The options here are the same as described in
{manpage}`taskdrc(5)` from the `taskwarrior2` package, but with one difference:
The `server` option is
`server.listen` here, because the
`server` option would collide with other options
like `server.cert` and we would run in a type error
(attribute set versus string).
Nix types like integers or booleans are automatically converted to
the right values Taskserver would expect.
'';
apply = let
mkKey = path: if path == ["server" "listen"] then "server"
else lib.concatStringsSep "." path;
recurse = path: attrs: let
mapper = name: val: let
newPath = path ++ [ name ];
scalar = if val == true then "true"
else if val == false then "false"
else toString val;
in if lib.isAttrs val then recurse newPath val
else [ "${mkKey newPath}=${scalar}" ];
in lib.concatLists (lib.mapAttrsToList mapper attrs);
in recurse [];
};
};
};
imports = [
(lib.mkRemovedOptionModule ["services" "taskserver" "extraConfig"] ''
This option was removed in favor of `services.taskserver.config` with
different semantics (it's now a list of attributes instead of lines).
Please look up the documentation of `services.taskserver.config' to get
more information about the new way to pass additional configuration
options.
'')
];
config = lib.mkMerge [
(lib.mkIf cfg.enable {
environment.systemPackages = [ nixos-taskserver ];
users.users = lib.optionalAttrs (cfg.user == "taskd") {
taskd = {
uid = config.ids.uids.taskd;
description = "Taskserver user";
group = cfg.group;
};
};
users.groups = lib.optionalAttrs (cfg.group == "taskd") {
taskd.gid = config.ids.gids.taskd;
};
services.taskserver.config = {
# systemd related
daemon = false;
log = "-";
# logging
debug = cfg.debug;
ip.log = cfg.ipLog;
# general
ciphers = cfg.ciphers;
confirmation = cfg.confirmation;
extensions = cfg.extensions;
queue.size = cfg.queueSize;
request.limit = cfg.requestLimit;
# client
client.allow = cfg.allowedClientIDs;
client.deny = cfg.disallowedClientIDs;
# server
trust = cfg.trust;
server = {
listen = "${cfg.listenHost}:${toString cfg.listenPort}";
} // (if needToCreateCA then {
cert = "${cfg.dataDir}/keys/server.cert";
key = "${cfg.dataDir}/keys/server.key";
crl = "${cfg.dataDir}/keys/server.crl";
} else {
cert = "${cfg.pki.manual.server.cert}";
key = "${cfg.pki.manual.server.key}";
${lib.mapNullable (_: "crl") cfg.pki.manual.server.crl} = "${cfg.pki.manual.server.crl}";
});
ca.cert = if needToCreateCA then "${cfg.dataDir}/keys/ca.cert"
else "${cfg.pki.manual.ca.cert}";
};
systemd.services.taskserver-init = {
wantedBy = [ "taskserver.service" ];
before = [ "taskserver.service" ];
description = "Initialize Taskserver Data Directory";
preStart = ''
mkdir -m 0770 -p "${cfg.dataDir}"
chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
'';
script = ''
${taskd} init
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;
serviceConfig.PrivateNetwork = true;
serviceConfig.PrivateDevices = true;
serviceConfig.PrivateTmp = true;
};
systemd.services.taskserver = {
description = "Taskwarrior 2 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 = let
mkCfgFlag = flag: lib.escapeShellArg "--${flag}";
cfgFlags = lib.concatMapStringsSep " " mkCfgFlag cfg.config;
in "@${taskd} taskd server ${cfgFlags}";
ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
Restart = "on-failure";
PermissionsStartOnly = true;
PrivateTmp = true;
PrivateDevices = true;
User = cfg.user;
Group = cfg.group;
};
};
})
(lib.mkIf (cfg.enable && needToCreateCA) {
systemd.services.taskserver-ca = {
wantedBy = [ "taskserver.service" ];
after = [ "taskserver-init.service" ];
before = [ "taskserver.service" ];
description = "Initialize CA for TaskServer";
serviceConfig.Type = "oneshot";
serviceConfig.UMask = "0077";
serviceConfig.PrivateNetwork = true;
serviceConfig.PrivateTmp = true;
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"
'';
};
})
(lib.mkIf (cfg.enable && cfg.openFirewall) {
networking.firewall.allowedTCPPorts = [ cfg.listenPort ];
})
];
meta.doc = ./default.md;
}