{ ... }: { _class = "clan.service"; manifest.name = "git-daemon"; manifest.description = "a really simple server for git repositories"; manifest.readme = "a really simple server for git repositories"; manifest.categories = [ "System" ]; roles.default = { description = "a default server role"; interface = { lib, ... }: { options = with lib; { directory = lib.mkOption { type = types.str; default = "/var/git"; }; repositories = lib.mkOption { type = with lib.types; attrsOf ( submodule ( { name, ... }: { options = { name = lib.mkOption { type = str; default = name; }; read-access = lib.mkOption { type = listOf str; default = [ ]; }; write-access = lib.mkOption { type = listOf str; default = [ ]; }; }; } ) ); default = { }; }; }; }; perInstance = { settings, ... }: { nixosModule = { pkgs, lib, config, ... }: { systemd.services.git-init = { serviceConfig = { Type = "oneshot"; User = config.services.gitDaemon.user; Group = config.services.gitDaemon.group; ExecStartPre = toString [ "+${pkgs.coreutils}/bin/install" "--directory" "--owner=${config.services.gitDaemon.user}" "--group=${config.services.gitDaemon.group}" "--mode=0750" settings.directory ]; ExecStart = let git-template = pkgs.stdenv.mkDerivation { name = "git-template"; buildCommand = '' cp --no-preserve=mode,ownership --recursive \ ${pkgs.git}/share/git-core/templates $out install -m550 $out/hooks/post-update{.sample,} ''; }; init-script = { name, ... }: pkgs.writeShellScript "git-init-${name}" '' ${pkgs.git}/bin/git init \ --bare --template=${git-template} --shared=0660 \ ${settings.directory}/${name}.git ${pkgs.git}/bin/git \ -C ${settings.directory}/${name}.git \ config set receive.denyNonFastforwards false ''; in map init-script (lib.attrValues settings.repositories); }; }; services.gitDaemon = { enable = true; user = "git"; group = "git"; options = let firewall = pkgs.writeText "git-daemon-firewall.json" ( builtins.toJSON (builtins.attrValues settings.repositories) ); hook = pkgs.writers.writePython3 "hook.py" { flakeIgnore = [ "E" ]; } '' import os, sys, enum, pathlib, ipaddress, json class Service(enum.Enum): UploadPack = enum.auto() ReceivePack = enum.auto() UploadArchive = enum.auto() @classmethod def parse(cls, string): return { 'upload-pack': cls.UploadPack, 'receive-pack': cls.ReceivePack, 'upload-archive': cls.UploadArchive }[string] @property def service(self): return { UploadPack: 'read-access', ReceivePack: 'write-access' }[self] UploadPack = Service.UploadPack ReceivePack = Service.ReceivePack def parse_remote_addr(remote_addr): if remote_addr.startswith('[') and remote_addr.endswith(']'): return ipaddress.ip_address(remote_addr[1:-1]) return ipaddress.ip_address(remote_addr) service = Service.parse(sys.argv[1]) repo = pathlib.Path(sys.argv[2]).stem client = parse_remote_addr(os.environ['REMOTE_ADDR']) with open("${firewall}", 'r') as f: firewall = json.load(f) for rule in firewall: if rule["name"] == repo: for network in rule[service.service]: if client in ipaddress.ip_network(network): sys.exit(0) print('stairway denied') sys.exit(1) ''; in toString [ "--enable=upload-pack" "--enable=receive-pack" "--disable=upload-archive" "--access-hook=${hook}" "--informative-errors" ]; exportAll = true; basePath = settings.directory; }; systemd.services.git-daemon = { requires = [ "git-init.service" ]; after = [ "git-init.service" ]; }; networking.firewall.allowedTCPPorts = [ 9418 ]; }; }; }; }