summaryrefslogtreecommitdiffstats
path: root/3modules/krebs/git.nix
diff options
context:
space:
mode:
Diffstat (limited to '3modules/krebs/git.nix')
-rw-r--r--3modules/krebs/git.nix490
1 files changed, 0 insertions, 490 deletions
diff --git a/3modules/krebs/git.nix b/3modules/krebs/git.nix
deleted file mode 100644
index 6046451..0000000
--- a/3modules/krebs/git.nix
+++ /dev/null
@@ -1,490 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-# TODO unify logging of shell scripts to user and journal
-# TODO move all scripts to ${etcDir}, so ControlMaster connections
-# immediately pick up new authenticators
-# TODO when authorized_keys changes, then restart ssh
-# (or kill already connected users somehow)
-
-with import ../../4lib/krebs { inherit lib; };
-let
- cfg = config.krebs.git;
-
- out = {
- # TODO don't import krebs.nginx here
- imports = [
- ../../3modules/krebs/nginx.nix
- ];
- options.krebs.git = api;
- config = mkIf cfg.enable (mkMerge [
- (mkIf cfg.cgit cgit-imp)
- git-imp
- ]);
- };
-
- api = {
- enable = mkEnableOption "krebs.git";
-
- cgit = mkOption {
- type = types.bool;
- default = true;
- description = ''
- Enable cgit.
- Cgit is an attempt to create a fast web interface for the git version
- control system, using a built in cache to decrease pressure on the
- git server.
- cgit in this module is being served via fastcgi nginx.This module
- deploys a http://cgit.<hostname> nginx configuration and enables nginx
- if not yet enabled.
- '';
- };
- dataDir = mkOption {
- type = types.str;
- default = "/var/lib/git";
- description = "Directory used to store repositories.";
- };
- etcDir = mkOption {
- type = types.str;
- default = "/etc/git";
- };
- 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;
- default = {};
- 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.
- '';
- };
- root-desc = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = ''
- Text printed below the heading on the repository index page.
- Default value: "a fast webinterface for the git dscm".
- '';
- };
- root-title = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = ''
- Text printed as heading on the repository index page.
- Default value: "Git Repository Browser".
- '';
- };
- rules = mkOption {
- type = types.unspecified;
- };
- };
-
- git-imp = {
- 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)
- config.krebs.users;
- uid = 129318403; # genid git
- };
- };
-
- cgit-imp = {
- users.extraUsers = lib.singleton {
- inherit (fcgitwrap-user) group name uid;
- home = toString (pkgs.runCommand "empty" {} "mkdir -p $out");
- };
-
- users.extraGroups = lib.singleton {
- inherit (fcgitwrap-group) gid name;
- };
-
- services.fcgiwrap = {
- enable = true;
- user = fcgitwrap-user.name;
- group = fcgitwrap-user.group;
- # socketAddress = "/run/fcgiwrap.sock" (default)
- # socketType = "unix" (default)
- };
-
- environment.etc."cgitrc".text = ''
- css=/static/cgit.css
- logo=/static/cgit.png
-
- # if you do not want that webcrawler (like google) index your site
- robots=noindex, nofollow
-
- virtual-root=/
-
- # TODO make this nicer (and/or somewhere else)
- cache-root=/tmp/cgit
-
- cache-size=1000
- enable-commit-graph=1
- enable-index-links=1
- enable-index-owner=0
- enable-log-filecount=1
- enable-log-linecount=1
- enable-remote-branches=1
-
- ${optionalString (cfg.root-title != null) "root-title=${cfg.root-title}"}
- ${optionalString (cfg.root-desc != null) "root-desc=${cfg.root-desc}"}
-
- snapshots=0
- max-stats=year
-
- ${concatMapStringsSep "\n" (repo: ''
- repo.url=${repo.name}
- repo.path=${cfg.dataDir}/${repo.name}
- ${optionalString (repo.section != null) "repo.section=${repo.section}"}
- ${optionalString (repo.desc != null) "repo.desc=${repo.desc}"}
- '') (filter isPublicRepo (attrValues cfg.repos))}
- '';
-
- system.activationScripts.cgit = ''
- mkdir -m 0700 -p /tmp/cgit
- chown ${toString fcgitwrap-user.uid}:${toString fcgitwrap-group.gid} /tmp/cgit
- '';
-
- krebs.nginx = {
- enable = true;
- servers.cgit = {
- server-names = [
- "cgit.${config.networking.hostName}"
- "cgit.${config.networking.hostName}.retiolum"
- ];
- locations = [
- (nameValuePair "/" ''
- include ${pkgs.nginx}/conf/fastcgi_params;
- fastcgi_param SCRIPT_FILENAME ${pkgs.cgit}/cgit/cgit.cgi;
- fastcgi_param PATH_INFO $uri;
- fastcgi_param QUERY_STRING $args;
- fastcgi_param HTTP_HOST $server_name;
- fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
- '')
- (nameValuePair "/static/" ''
- root ${pkgs.cgit}/cgit;
- rewrite ^/static(/.*)$ $1 break;
- '')
- ];
- };
- };
- };
-
- fcgitwrap-user = {
- name = "fcgiwrap";
- uid = 2867890860; # genid fcgiwrap
- group = "fcgiwrap";
- };
-
- fcgitwrap-group = {
- name = fcgitwrap-user.name;
- gid = fcgitwrap-user.uid;
- };
-
-
- 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 = pkgs.writeScript "${farm-name}_${script-name}" script-string;
- };
- in
- pkgs.linkFarm farm-name (mapAttrsToList makeScript scripts);
-
-
- git-ssh-command = pkgs.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 = pkgs.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
-out