diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix index 62e35803117..86eabb9bcfc 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 00000000000..3277a50cd51 --- /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 > "{}" <