diff options
Diffstat (limited to 'modules/tv')
| -rw-r--r-- | modules/tv/git/cgit.nix | 11 | ||||
| -rw-r--r-- | modules/tv/git/config.nix | 272 | ||||
| -rw-r--r-- | modules/tv/git/default.nix | 374 | ||||
| -rw-r--r-- | modules/tv/git/options.nix | 93 | ||||
| -rw-r--r-- | modules/tv/git/public.nix | 3 | 
5 files changed, 377 insertions, 376 deletions
diff --git a/modules/tv/git/cgit.nix b/modules/tv/git/cgit.nix index fd87b9081..826ee7f27 100644 --- a/modules/tv/git/cgit.nix +++ b/modules/tv/git/cgit.nix @@ -1,23 +1,15 @@ -{ config, lib, pkgs, ... }: +{ cfg, config, lib, pkgs, ... }:  let    inherit (builtins) attrValues filter getAttr;    inherit (lib) concatMapStringsSep mkIf optionalString; -  cfg = config.services.git; -    location = lib.nameValuePair; # TODO this is also in modules/wu/default.nix    isPublicRepo = getAttr "public"; # TODO this is also in ./default.nix  in  { -  imports = [ -    ../../tv/nginx -  ]; - -  config = mkIf cfg.cgit { -      users.extraUsers = lib.singleton {        name = "fcgiwrap";        uid = 2851179180; # genid fcgiwrap @@ -93,5 +85,4 @@ in          '')        ];      }; -  };  } diff --git a/modules/tv/git/config.nix b/modules/tv/git/config.nix new file mode 100644 index 000000000..4f44c3839 --- /dev/null +++ b/modules/tv/git/config.nix @@ -0,0 +1,272 @@ +arg@{ cfg, lib, pkgs, ... }: + +let +  inherit (builtins) head tail typeOf; +  inherit (lib) +    attrValues concatStringsSep concatMapStringsSep escapeShellArg filter +    getAttr hasAttr hasPrefix lessThan makeSearchPath mapAttrsToList +    optional optionalString removePrefix singleton sort unique; +  inherit (pkgs) linkFarm writeScript; + +  ensureList = x: +    if typeOf x == "list" then x else [x]; + +  getName = x: x.name; + +  isPublicRepo = getAttr "public"; # TODO this is also in ./cgit.nix + +  makeAuthorizedKey = git-ssh-command: user@{ name, pubkey }: +    # TODO assert name +    # TODO assert pubkey +    let +      options = concatStringsSep "," [ +        ''command="exec ${git-ssh-command} ${name}"'' +        "no-agent-forwarding" +        "no-port-forwarding" +        "no-pty" +        "no-X11-forwarding" +      ]; +    in +    "${options} ${pubkey}"; + +  # [case-pattern] -> shell-script +  # Create a shell script that succeeds (exit 0) when all its arguments +  # match the case patterns (in the given order). +  makeAuthorizeScript = +    let +      # TODO escape +      to-pattern = x: concatStringsSep "|" (ensureList x); +      go = i: ps: +        if ps == [] +          then "exit 0" +          else '' +            case ''$${toString i} in ${to-pattern (head ps)}) +            ${go (i + 1) (tail ps)} +            esac''; +    in +    patterns: '' +      #! /bin/sh +      set -euf +      ${concatStringsSep "\n" (map (go 1) patterns)} +      exit -1 +    ''; + +  reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); + +  # TODO makeGitHooks that uses runCommand instead of scriptFarm? +  scriptFarm = +    farm-name: scripts: +    let +      makeScript = script-name: script-string: { +        name = script-name; +        path = writeScript "${farm-name}_${script-name}" script-string; +      }; +    in +    linkFarm farm-name (mapAttrsToList makeScript scripts); + + +  git-ssh-command = writeScript "git-ssh-command" '' +    #! /bin/sh +    set -euf + +    PATH=${makeSearchPath "bin" (with pkgs; [ +      coreutils +      git +      gnugrep +      gnused +      systemd +    ])} + +    abort() { +      echo "error: $1" >&2 +      systemd-cat -p err -t git echo "error: $1" +      exit -1 +    } + +    GIT_SSH_USER=$1 + +    systemd-cat -p info -t git echo \ +      "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" + +    # References: The Base Definitions volume of +    # POSIX.1‐2013, Section 3.278, Portable Filename Character Set +    portable_filename_bre="^[A-Za-z0-9._-]\\+$" + +    command=$(echo "$SSH_ORIGINAL_COMMAND" \ +      | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ +      | grep "$portable_filename_bre" \ +      || abort 'cannot read command') + +    GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ +      | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ +      | grep "$portable_filename_bre" \ +      || abort 'cannot read reponame') + +    ${cfg.etcDir}/authorize-command \ +        "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ +      || abort 'access denied' + +    repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO + +    systemd-cat -p info -t git \ +      echo "authorized exec $command $repodir" + +    export GIT_SSH_USER +    export GIT_SSH_REPO +    exec "$command" "$repodir" +  ''; + +  init-script = writeScript "git-init" '' +    #! /bin/sh +    set -euf + +    PATH=${makeSearchPath "bin" (with pkgs; [ +      coreutils +      findutils +      gawk +      git +      gnugrep +      gnused +    ])} + +    dataDir=${escapeShellArg cfg.dataDir} +    mkdir -p "$dataDir" + +    # Notice how the presence of hooks symlinks determine whether +    # we manage a repositry or not. + +    # Make sure that no existing repository has hooks.  We can delete +    # symlinks because we assume we created them. +    find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete +    bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) +    if echo "$bad_hooks" | grep -q .; then +      printf 'error: unknown hooks:\n%s\n' \ +        "$(echo "$bad_hooks" | sed 's/^/  /')" \ +        >&2 +      exit -1 +    fi + +    # Initialize repositories. +    ${concatMapStringsSep "\n" (repo: +      let +        hooks = scriptFarm "git-hooks" (makeHooks repo); +      in +      '' +        reponame=${escapeShellArg repo.name} +        repodir=$dataDir/$reponame +        mode=${toString (if isPublicRepo repo then 0711 else 0700)} +        if ! test -d "$repodir"; then +          mkdir -m "$mode" "$repodir" +          git init --bare --template=/var/empty "$repodir" +          chown -R git:nogroup "$repodir" +        fi +        ln -s ${hooks} "$repodir/hooks" +      '' +    ) (attrValues cfg.repos)} + +    # Warn about repositories that exist but aren't mentioned in the +    # current configuration (and thus didn't receive a hooks symlink). +    unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ +      -type d \! -exec test -e '{}/hooks' \; -print) +    if echo "$unknown_repos" | grep -q .; then +      printf 'warning: stale repositories:\n%s\n' \ +        "$(echo "$unknown_repos" | sed 's/^/  /')" \ +        >&2 +    fi +  ''; + +  makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { +    pre-receive = '' +      #! /bin/sh +      set -euf + +      PATH=${makeSearchPath "bin" (with pkgs; [ +        coreutils # env +        git +        systemd +      ])} + +      accept() { +        #systemd-cat -p info -t git echo "authorized $1" +        accept_string="''${accept_string+$accept_string +      }authorized $1" +      } +      reject() { +        #systemd-cat -p err -t git echo "denied $1" +        #echo 'access denied' >&2 +        #exit_code=-1 +        reject_string="''${reject_string+$reject_string +      }access denied: $1" +      } + +      empty=0000000000000000000000000000000000000000 + +      accept_string= +      reject_string= +      while read oldrev newrev ref; do + +        if [ $oldrev = $empty ]; then +          receive_mode=create +        elif [ $newrev = $empty ]; then +          receive_mode=delete +        elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then +          receive_mode=fast-forward +        else +          receive_mode=non-fast-forward +        fi + +        if ${cfg.etcDir}/authorize-push \ +            "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then +          accept "$receive_mode $ref" +        else +          reject "$receive_mode $ref" +        fi +      done + +      if [ -n "$reject_string" ]; then +        systemd-cat -p err -t git echo "$reject_string" +        exit -1 +      fi + +      systemd-cat -p info -t git echo "$accept_string" + +      ${optionalString (hasAttr "post-receive" repo.hooks) '' +        # custom post-receive hook +        ${repo.hooks.post-receive}''} +    ''; +  }; + +  etc-base = +    assert (hasPrefix "/etc/" cfg.etcDir); +    removePrefix "/etc/" cfg.etcDir; +in + +{ +  system.activationScripts.git-init = "${init-script}"; +   +  # TODO maybe put all scripts here and then use PATH? +  environment.etc."${etc-base}".source = +    scriptFarm "git-ssh-authorizers" { +      authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ +        (map getName (ensureList user)) +        (map getName (ensureList repo)) +        (map getName perm.allow-commands) +      ]) cfg.rules); +   +      authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ +        (map getName (ensureList user)) +        (map getName (ensureList repo)) +        (ensureList perm.allow-receive-ref) +        (map getName perm.allow-receive-modes) +      ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); +    }; +   +  users.extraUsers = singleton { +    description = "Git repository hosting user"; +    name = "git"; +    shell = "/bin/sh"; +    openssh.authorizedKeys.keys = +      mapAttrsToList (_: makeAuthorizedKey git-ssh-command) cfg.users; +    uid = 112606723; # genid git +  }; +} diff --git a/modules/tv/git/default.nix b/modules/tv/git/default.nix index edaa80f41..17bc3738b 100644 --- a/modules/tv/git/default.nix +++ b/modules/tv/git/default.nix @@ -1,76 +1,10 @@ -{ config, lib, pkgs, ... }: +arg@{ config, pkgs, lib, ... }:  let -  inherit (builtins) -    attrNames attrValues concatLists getAttr filter hasAttr head lessThan -    removeAttrs tail toJSON typeOf; -  inherit (lib) -    concatMapStringsSep concatStringsSep escapeShellArg hasPrefix -    literalExample makeSearchPath mapAttrsToList mkIf mkOption optionalString -    removePrefix singleton sort types unique; -  inherit (pkgs) linkFarm writeScript writeText; +  inherit (lib) mkIf mkMerge; - -  ensureList = x: -    if typeOf x == "list" then x else [x]; - -  getName = x: x.name; - -  isPublicRepo = getAttr "public"; # TODO this is also in ./cgit.nix - -  makeAuthorizedKey = git-ssh-command: user@{ name, pubkey }: -    # TODO assert name -    # TODO assert pubkey -    let -      options = concatStringsSep "," [ -        ''command="exec ${git-ssh-command} ${name}"'' -        "no-agent-forwarding" -        "no-port-forwarding" -        "no-pty" -        "no-X11-forwarding" -      ]; -    in -    "${options} ${pubkey}"; - -  # [case-pattern] -> shell-script -  # Create a shell script that succeeds (exit 0) when all its arguments -  # match the case patterns (in the given order). -  makeAuthorizeScript = -    let -      # TODO escape -      to-pattern = x: concatStringsSep "|" (ensureList x); -      go = i: ps: -        if ps == [] -          then "exit 0" -          else '' -            case ''$${toString i} in ${to-pattern (head ps)}) -            ${go (i + 1) (tail ps)} -            esac''; -    in -    patterns: '' -      #! /bin/sh -      set -euf -      ${concatStringsSep "\n" (map (go 1) patterns)} -      exit -1 -    ''; - -  reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); - -  # TODO makeGitHooks that uses runCommand instead of scriptFarm? -  scriptFarm = -    farm-name: scripts: -    let -      makeScript = script-name: script-string: { -        name = script-name; -        path = writeScript "${farm-name}_${script-name}" script-string; -      }; -    in -    linkFarm farm-name (mapAttrsToList makeScript scripts); - -  writeJSON = name: data: writeText name (toJSON data); - - -  cfg = config.services.git; +  cfg = config.tv.git; +  arg' = arg // { inherit cfg; };  in  # TODO unify logging of shell scripts to user and journal @@ -81,301 +15,13 @@ in  {    imports = [ -    ./cgit.nix +    ../../tv/nginx    ]; -  options.services.git = { -    enable = mkOption { -      type = types.bool; -      default = false; -      description = "Enable Git repository hosting."; -    }; -    cgit = mkOption { -      type = types.bool; -      default = true; -      description = "Enable cgit."; # TODO better desc; talk about nginx -    }; -    dataDir = mkOption { -      type = types.str; -      default = "/var/lib/git"; -      description = "Directory used to store repositories."; -    }; -    etcDir = mkOption { -      type = types.str; -      default = "/etc/git"; -    }; -    rules = mkOption { -      type = types.unspecified; -    }; -    repos = mkOption { -      type = types.attrsOf (types.submodule ({ -        options = { -          desc = mkOption { -            type = types.nullOr types.str; -            default = null; -            description = '' -              Repository description. -            ''; -          }; -          section = mkOption { -            type = types.nullOr types.str; -            default = null; -            description = '' -              Repository section. -            ''; -          }; -          name = mkOption { -            type = types.str; -            description = '' -              Repository name. -            ''; -          }; -          hooks = mkOption { -            type = types.attrsOf types.str; -            description = '' -              Repository-specific hooks. -            ''; -          }; -          public = mkOption { -            type = types.bool; -            default = false; -            description = '' -              Allow everybody to read the repository via HTTP if cgit enabled. -            ''; -            # TODO allow every configured user to fetch the repository via SSH. -          }; -        }; -      })); - -      default = {}; - -      example = literalExample '' -        { -          testing = { -            name = "testing"; -            hooks.post-update = ''' -              #! /bin/sh -              set -euf -              echo post-update hook: $* >&2 -            '''; -          }; -          testing2 = { name = "testing2"; }; -        } -      ''; - -      description = '' -        Repositories. -      ''; -    }; -    users = mkOption { -      type = types.unspecified; -    }; -  }; - -  config = -    let -      git-ssh-command = writeScript "git-ssh-command" '' -        #! /bin/sh -        set -euf - -        PATH=${makeSearchPath "bin" (with pkgs; [ -          coreutils -          git -          gnugrep -          gnused -          systemd -        ])} - -        abort() { -          echo "error: $1" >&2 -          systemd-cat -p err -t git echo "error: $1" -          exit -1 -        } - -        GIT_SSH_USER=$1 - -        systemd-cat -p info -t git echo \ -          "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" - -        # References: The Base Definitions volume of -        # POSIX.1‐2013, Section 3.278, Portable Filename Character Set -        portable_filename_bre="^[A-Za-z0-9._-]\\+$" - -        command=$(echo "$SSH_ORIGINAL_COMMAND" \ -          | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ -          | grep "$portable_filename_bre" \ -          || abort 'cannot read command') - -        GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ -          | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ -          | grep "$portable_filename_bre" \ -          || abort 'cannot read reponame') - -        ${cfg.etcDir}/authorize-command \ -            "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ -          || abort 'access denied' - -        repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO - -        systemd-cat -p info -t git \ -          echo "authorized exec $command $repodir" - -        export GIT_SSH_USER -        export GIT_SSH_REPO -        exec "$command" "$repodir" -      ''; - -      init-script = writeScript "git-init" '' -        #! /bin/sh -        set -euf - -        PATH=${makeSearchPath "bin" (with pkgs; [ -          coreutils -          findutils -          gawk -          git -          gnugrep -          gnused -        ])} - -        dataDir=${escapeShellArg cfg.dataDir} -        mkdir -p "$dataDir" - -        # Notice how the presence of hooks symlinks determine whether -        # we manage a repositry or not. - -        # Make sure that no existing repository has hooks.  We can delete -        # symlinks because we assume we created them. -        find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete -        bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) -        if echo "$bad_hooks" | grep -q .; then -          printf 'error: unknown hooks:\n%s\n' \ -            "$(echo "$bad_hooks" | sed 's/^/  /')" \ -            >&2 -          exit -1 -        fi - -        # Initialize repositories. -        ${concatMapStringsSep "\n" (repo: -          let -            hooks = scriptFarm "git-hooks" (makeHooks repo); -          in -          '' -            reponame=${escapeShellArg repo.name} -            repodir=$dataDir/$reponame -            mode=${toString (if isPublicRepo repo then 0711 else 0700)} -            if ! test -d "$repodir"; then -              mkdir -m "$mode" "$repodir" -              git init --bare --template=/var/empty "$repodir" -              chown -R git:nogroup "$repodir" -            fi -            ln -s ${hooks} "$repodir/hooks" -          '' -        ) (attrValues cfg.repos)} - -        # Warn about repositories that exist but aren't mentioned in the -        # current configuration (and thus didn't receive a hooks symlink). -        unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ -          -type d \! -exec test -e '{}/hooks' \; -print) -        if echo "$unknown_repos" | grep -q .; then -          printf 'warning: stale repositories:\n%s\n' \ -            "$(echo "$unknown_repos" | sed 's/^/  /')" \ -            >&2 -        fi -      ''; - -      makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { -        pre-receive = '' -          #! /bin/sh -          set -euf - -          PATH=${makeSearchPath "bin" (with pkgs; [ -            coreutils # env -            git -            systemd -          ])} - -          accept() { -            #systemd-cat -p info -t git echo "authorized $1" -            accept_string="''${accept_string+$accept_string -          }authorized $1" -          } -          reject() { -            #systemd-cat -p err -t git echo "denied $1" -            #echo 'access denied' >&2 -            #exit_code=-1 -            reject_string="''${reject_string+$reject_string -          }access denied: $1" -          } - -          empty=0000000000000000000000000000000000000000 - -          accept_string= -          reject_string= -          while read oldrev newrev ref; do - -            if [ $oldrev = $empty ]; then -              receive_mode=create -            elif [ $newrev = $empty ]; then -              receive_mode=delete -            elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then -              receive_mode=fast-forward -            else -              receive_mode=non-fast-forward -            fi - -            if ${cfg.etcDir}/authorize-push \ -                "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then -              accept "$receive_mode $ref" -            else -              reject "$receive_mode $ref" -            fi -          done - -          if [ -n "$reject_string" ]; then -            systemd-cat -p err -t git echo "$reject_string" -            exit -1 -          fi - -          systemd-cat -p info -t git echo "$accept_string" - -          ${optionalString (hasAttr "post-receive" repo.hooks) '' -            # custom post-receive hook -            ${repo.hooks.post-receive}''} -        ''; -      }; - -      etc-base = -        assert (hasPrefix "/etc/" cfg.etcDir); -        removePrefix "/etc/" cfg.etcDir; -    in -    mkIf cfg.enable { -      system.activationScripts.git-init = "${init-script}"; - -      # TODO maybe put all scripts here and then use PATH? -      environment.etc."${etc-base}".source = -        scriptFarm "git-ssh-authorizers" { -          authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ -            (map getName (ensureList user)) -            (map getName (ensureList repo)) -            (map getName perm.allow-commands) -          ]) cfg.rules); - -          authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ -            (map getName (ensureList user)) -            (map getName (ensureList repo)) -            (ensureList perm.allow-receive-ref) -            (map getName perm.allow-receive-modes) -          ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); -        }; +  options.tv.git = import ./options.nix arg'; -      users.extraUsers = singleton { -        description = "Git repository hosting user"; -        name = "git"; -        shell = "/bin/sh"; -        openssh.authorizedKeys.keys = -          mapAttrsToList (_: makeAuthorizedKey git-ssh-command) cfg.users; -        uid = 112606723; # genid git -      }; -    }; +  config = mkIf cfg.enable (mkMerge [ +    (import ./config.nix arg') +    (mkIf cfg.cgit (import ./cgit.nix arg')) +  ]);  } diff --git a/modules/tv/git/options.nix b/modules/tv/git/options.nix new file mode 100644 index 000000000..c251d7d4c --- /dev/null +++ b/modules/tv/git/options.nix @@ -0,0 +1,93 @@ +{ lib, ... }:  + +let +  inherit (lib) literalExample mkOption types; +in + +{ +  enable = mkOption { +    type = types.bool; +    default = false; +    description = "Enable Git repository hosting."; +  }; +  cgit = mkOption { +    type = types.bool; +    default = true; +    description = "Enable cgit."; # TODO better desc; talk about nginx +  }; +  dataDir = mkOption { +    type = types.str; +    default = "/var/lib/git"; +    description = "Directory used to store repositories."; +  }; +  etcDir = mkOption { +    type = types.str; +    default = "/etc/git"; +  }; +  rules = mkOption { +    type = types.unspecified; +  }; +  repos = mkOption { +    type = types.attrsOf (types.submodule ({ +      options = { +        desc = mkOption { +          type = types.nullOr types.str; +          default = null; +          description = '' +            Repository description. +          ''; +        }; +        section = mkOption { +          type = types.nullOr types.str; +          default = null; +          description = '' +            Repository section. +          ''; +        }; +        name = mkOption { +          type = types.str; +          description = '' +            Repository name. +          ''; +        }; +        hooks = mkOption { +          type = types.attrsOf types.str; +          description = '' +            Repository-specific hooks. +          ''; +        }; +        public = mkOption { +          type = types.bool; +          default = false; +          description = '' +            Allow everybody to read the repository via HTTP if cgit enabled. +          ''; +          # TODO allow every configured user to fetch the repository via SSH. +        }; +      }; +    })); + +    default = {}; + +    example = literalExample '' +      { +        testing = { +          name = "testing"; +          hooks.post-update = ''' +            #! /bin/sh +            set -euf +            echo post-update hook: $* >&2 +          '''; +        }; +        testing2 = { name = "testing2"; }; +      } +    ''; + +    description = '' +      Repositories. +    ''; +  }; +  users = mkOption { +    type = types.unspecified; +  }; +} diff --git a/modules/tv/git/public.nix b/modules/tv/git/public.nix index 7dc93e821..09d1e6b4b 100644 --- a/modules/tv/git/public.nix +++ b/modules/tv/git/public.nix @@ -65,8 +65,7 @@ in    imports = [      ./.    ]; - -  services.git = { +  tv.git = {      enable = true;      inherit repos rules users;    };  | 
