kore / nixos / roles / git.nix196 lines · 6482B · raw
1{ inputs, config, lib, pkgs, ... }:
2
3let
4 inherit (config.kore) identities repos;
5 inherit (config.networking) domain;
6 root = "/srv/git";
7
8 petal = pkgs.stdenv.mkDerivation {
9 pname = "petal";
10 version = "0-${inputs.greenhouse.shortRev or "dirty"}";
11
12 src = "${inputs.greenhouse}/petal";
13
14 buildInputs = [ pkgs.libgit2 ];
15
16 makeFlags = [
17 "PREFIX=$(out)"
18 "CC=${pkgs.stdenv.cc.targetPrefix}cc"
19 "GIT2INC=${lib.getDev pkgs.libgit2}/include"
20 "GIT2LIB=${lib.getLib pkgs.libgit2}/lib"
21 ];
22 };
23
24 petalHome = pkgs.writeTextDir ".gitconfig" (lib.generators.toGitINI {
25 safe.directory = map (repo: "${root}/${repo}.git") (lib.attrNames repos);
26 });
27
28 mirrorKey = "${root}/.ssh/mirror";
29 mirrors = lib.any (access: access.mirror != null) (lib.attrValues repos);
30
31 mirrorHook = pkgs.writeShellScript "kore-git-mirror" ''
32 cat > /dev/null
33 url=$(${pkgs.git}/bin/git config --get kore.mirror) || exit 0
34 export GIT_SSH_COMMAND="${pkgs.openssh}/bin/ssh -i ${mirrorKey} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes"
35 if ${pkgs.git}/bin/git push --mirror --quiet "$url"; then
36 echo "kore-git: mirrored to $url"
37 else
38 echo "kore-git: mirror to $url failed" >&2
39 fi
40 '';
41
42 fastcgi = ''
43 fastcgi_param GATEWAY_INTERFACE CGI/1.1;
44 fastcgi_param SERVER_PROTOCOL $server_protocol;
45 fastcgi_param SERVER_NAME $server_name;
46 fastcgi_param SERVER_PORT $server_port;
47 fastcgi_param REQUEST_METHOD $request_method;
48 fastcgi_param REQUEST_URI $request_uri;
49 fastcgi_param QUERY_STRING $query_string;
50 fastcgi_param CONTENT_TYPE $content_type;
51 fastcgi_param CONTENT_LENGTH $content_length;
52 fastcgi_param REMOTE_ADDR $remote_addr;
53 fastcgi_param HTTPS $https if_not_empty;
54 fastcgi_param HOME ${petalHome};
55 fastcgi_pass unix:${config.services.fcgiwrap.instances.petal.socket.address};
56 '';
57
58 owner = access: if access.owner != null then access.owner else lib.findFirst (_: true) "" access.write;
59
60 acl = pkgs.writeText "kore-git-acl" (lib.concatLines (lib.concatLists (lib.mapAttrsToList (repo: access:
61 map (id: "${repo} write ${id}") access.write
62 ++ map (id: "${repo} read ${id}") access.read
63 ) repos)));
64
65 kore-git = pkgs.writeShellApplication {
66 name = "kore-git";
67 runtimeInputs = [ pkgs.git pkgs.gnugrep ];
68 text = ''
69 id="$1"
70 cmd="''${SSH_ORIGINAL_COMMAND:-}"
71
72 deny() {
73 echo "kore-git: access denied" >&2
74 exit 1
75 }
76
77 verb="''${cmd%% *}"
78 case "$verb" in
79 git-upload-pack | git-upload-archive) perm="read" ;;
80 git-receive-pack) perm="write" ;;
81 *)
82 echo "kore-git: hi $id, shell access is not provided" >&2
83 exit 1
84 ;;
85 esac
86
87 path="''${cmd#"$verb" }"
88 path="''${path#\'}"
89 path="''${path%\'}"
90 path="''${path#/}"
91 path="''${path%/}"
92 path="''${path%.git}"
93
94 [[ "$path" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*$ ]] || deny
95
96 grep -qFx "$path write $id" ${acl} \
97 || { [ "$perm" = read ] && grep -qFx "$path read $id" ${acl}; } \
98 || deny
99
100 exec git-shell -c "$verb '${root}/$path.git'"
101 '';
102 };
103
104 members = lib.unique (lib.concatLists (lib.mapAttrsToList (_: access: access.write ++ access.read) repos));
105in {
106 assertions = [{
107 assertion = lib.elem "web" config.kore.currentHost.roles;
108 message = "kore: the git role needs the web role for petal";
109 }];
110
111 users.groups.git = {};
112 users.users.git = {
113 isSystemUser = true;
114 group = "git";
115 home = root;
116 homeMode = "750";
117 createHome = true;
118 shell = pkgs.bash;
119
120 openssh.authorizedKeys.keys = lib.concatMap (id:
121 map (key: ''command="${kore-git}/bin/kore-git ${id}",restrict ${key}'') (identities.${id}.sshKeys or [])
122 ) members;
123 };
124
125 systemd.services.kore-git-repos = {
126 wantedBy = [ "multi-user.target" ];
127 after = [ "local-fs.target" ];
128 serviceConfig = {
129 Type = "oneshot";
130 User = "git";
131 Group = "git";
132 };
133 path = [ pkgs.git pkgs.coreutils pkgs.openssh ];
134 script = lib.optionalString mirrors ''
135 if [ ! -f '${mirrorKey}' ]; then
136 mkdir -p -m 700 '${root}/.ssh'
137 ssh-keygen -q -t ed25519 -N "" -C "git@${config.networking.hostName} mirror" -f '${mirrorKey}'
138 fi
139 '' + lib.concatStrings (lib.mapAttrsToList (repo: access: ''
140 if [ ! -d '${root}/${repo}.git' ]; then
141 mkdir -p "$(dirname '${root}/${repo}.git')"
142 git init --quiet --bare --initial-branch=main '${root}/${repo}.git'
143 fi
144 printf '%s\n' ${lib.escapeShellArg access.description} > '${root}/${repo}.git/description'
145 printf '%s\n' ${lib.escapeShellArg (owner access)} > '${root}/${repo}.git/owner'
146 ln -sfn ${mirrorHook} '${root}/${repo}.git/hooks/post-receive'
147 ${if access.mirror != null
148 then "git -C '${root}/${repo}.git' config kore.mirror ${lib.escapeShellArg access.mirror}"
149 else "git -C '${root}/${repo}.git' config --unset kore.mirror || true"}
150 ${if access.private
151 then "rm -f '${root}/${repo}.git/git-daemon-export-ok'"
152 else "touch '${root}/${repo}.git/git-daemon-export-ok'"}
153 '') repos);
154 };
155
156 programs.ssh.knownHosts."github.com".publicKey =
157 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl";
158
159 systemd.tmpfiles.rules = [ "z ${root} 0750 git git -" ];
160
161 users.users.petal = {
162 isSystemUser = true;
163 group = "git";
164 };
165
166 services.fcgiwrap.instances.petal = {
167 process.user = "petal";
168 process.group = "git";
169 socket.user = config.services.nginx.user;
170 socket.group = config.services.nginx.group;
171 };
172
173 services.nginx.virtualHosts."git.${domain}" = {
174 enableACME = true;
175 forceSSL = true;
176
177 locations."= /style.css".alias = "${petal}/share/petal/style.css";
178 locations."= /icon.svg".alias = "${petal}/share/petal/icon.svg";
179
180 locations."/".extraConfig = fastcgi + ''
181 fastcgi_param SCRIPT_FILENAME ${petal}/bin/petal;
182 fastcgi_param SCRIPT_NAME "";
183 fastcgi_param PATH_INFO $uri;
184 '';
185
186 locations."~ ^/(.+\\.git/(?:info/refs|git-upload-pack))$".extraConfig = ''
187 if ($arg_service = git-receive-pack) {
188 return 403;
189 }
190 '' + fastcgi + ''
191 fastcgi_param SCRIPT_FILENAME ${pkgs.git}/libexec/git-core/git-http-backend;
192 fastcgi_param PATH_INFO /$1;
193 fastcgi_param GIT_PROJECT_ROOT ${root};
194 '';
195 };
196}