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";