kore / nixos / roles / git.nix151 lines · 4573B · 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 owner = access: if access.owner != null then access.owner else lib.findFirst (_: true) "" access.write;
29
30 acl = pkgs.writeText "kore-git-acl" (lib.concatLines (lib.concatLists (lib.mapAttrsToList (repo: access:
31 map (id: "${repo} write ${id}") access.write
32 ++ map (id: "${repo} read ${id}") access.read
33 ) repos)));
34
35 kore-git = pkgs.writeShellApplication {
36 name = "kore-git";
37 runtimeInputs = [ pkgs.git pkgs.gnugrep ];
38 text = ''
39 id="$1"
40 cmd="''${SSH_ORIGINAL_COMMAND:-}"
41
42 deny() {
43 echo "kore-git: access denied" >&2
44 exit 1
45 }
46
47 verb="''${cmd%% *}"
48 case "$verb" in
49 git-upload-pack | git-upload-archive) perm="read" ;;
50 git-receive-pack) perm="write" ;;
51 *)
52 echo "kore-git: hi $id, shell access is not provided" >&2
53 exit 1
54 ;;
55 esac
56
57 path="''${cmd#"$verb" }"
58 path="''${path#\'}"
59 path="''${path%\'}"
60 path="''${path#/}"
61 path="''${path%/}"
62 path="''${path%.git}"
63
64 [[ "$path" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*$ ]] || deny
65
66 grep -qFx "$path write $id" ${acl} \
67 || { [ "$perm" = read ] && grep -qFx "$path read $id" ${acl}; } \
68 || deny
69
70 exec git-shell -c "$verb '${root}/$path.git'"
71 '';
72 };
73
74 members = lib.unique (lib.concatLists (lib.mapAttrsToList (_: access: access.write ++ access.read) repos));
75in {
76 assertions = [{
77 assertion = lib.elem "web" config.kore.currentHost.roles;
78 message = "kore: the git role needs the web role for petal";
79 }];
80
81 users.groups.git = {};
82 users.users.git = {
83 isSystemUser = true;
84 group = "git";
85 home = root;
86 homeMode = "750";
87 createHome = true;
88 shell = pkgs.bash;
89
90 openssh.authorizedKeys.keys = lib.concatMap (id:
91 map (key: ''command="${kore-git}/bin/kore-git ${id}",restrict ${key}'') (identities.${id}.sshKeys or [])
92 ) members;
93 };
94
95 systemd.services.kore-git-repos = {
96 wantedBy = [ "multi-user.target" ];
97 after = [ "local-fs.target" ];
98 serviceConfig = {
99 Type = "oneshot";
100 User = "git";
101 Group = "git";
102 };
103 path = [ pkgs.git pkgs.coreutils ];
104 script = lib.concatStrings (lib.mapAttrsToList (repo: access: ''
105 if [ ! -d '${root}/${repo}.git' ]; then
106 mkdir -p "$(dirname '${root}/${repo}.git')"
107 git init --quiet --bare --initial-branch=main '${root}/${repo}.git'
108 fi
109 printf '%s\n' ${lib.escapeShellArg access.description} > '${root}/${repo}.git/description'
110 printf '%s\n' ${lib.escapeShellArg (owner access)} > '${root}/${repo}.git/owner'
111 '') repos);
112 };
113
114 systemd.tmpfiles.rules = [ "z ${root} 0750 git git -" ];
115
116 users.users.petal = {
117 isSystemUser = true;
118 group = "git";
119 };
120
121 services.fcgiwrap.instances.petal = {
122 process.user = "petal";
123 process.group = "git";
124 socket.user = config.services.nginx.user;
125 socket.group = config.services.nginx.group;
126 };
127
128 services.nginx.virtualHosts."git.${domain}" = {
129 enableACME = true;
130 forceSSL = true;
131
132 locations."= /style.css".alias = "${petal}/share/petal/style.css";
133
134 locations."/".extraConfig = ''
135 fastcgi_param GATEWAY_INTERFACE CGI/1.1;
136 fastcgi_param SERVER_PROTOCOL $server_protocol;
137 fastcgi_param SERVER_NAME $server_name;
138 fastcgi_param SERVER_PORT $server_port;
139 fastcgi_param REQUEST_METHOD $request_method;
140 fastcgi_param REQUEST_URI $request_uri;
141 fastcgi_param QUERY_STRING $query_string;
142 fastcgi_param REMOTE_ADDR $remote_addr;
143 fastcgi_param HTTPS $https if_not_empty;
144 fastcgi_param SCRIPT_FILENAME ${petal}/bin/petal;
145 fastcgi_param SCRIPT_NAME "";
146 fastcgi_param PATH_INFO $uri;
147 fastcgi_param HOME ${petalHome};
148 fastcgi_pass unix:${config.services.fcgiwrap.instances.petal.socket.address};
149 '';
150 };
151}