at 25.11-pre 6.4 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 9 cfg = config.services.froide-govplan; 10 pythonFmt = pkgs.formats.pythonVars { }; 11 settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings; 12 13 pkg = cfg.package.overridePythonAttrs (old: { 14 postInstall = 15 old.postInstall 16 + '' 17 ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py 18 ''; 19 }); 20 21 froide-govplan = pkgs.writeShellApplication { 22 name = "froide-govplan"; 23 runtimeInputs = [ pkgs.coreutils ]; 24 text = '' 25 SUDO="exec" 26 if [[ "$USER" != govplan ]]; then 27 SUDO="exec /run/wrappers/bin/sudo -u govplan" 28 fi 29 $SUDO env ${lib.getExe pkg} "$@" 30 ''; 31 }; 32 33 # Service hardening 34 defaultServiceConfig = { 35 # Secure the services 36 ReadWritePaths = [ cfg.dataDir ]; 37 CacheDirectory = "froide-govplan"; 38 CapabilityBoundingSet = ""; 39 # ProtectClock adds DeviceAllow=char-rtc r 40 DeviceAllow = ""; 41 LockPersonality = true; 42 MemoryDenyWriteExecute = true; 43 NoNewPrivileges = true; 44 PrivateDevices = true; 45 PrivateMounts = true; 46 PrivateTmp = true; 47 PrivateUsers = true; 48 ProtectClock = true; 49 ProtectHome = true; 50 ProtectHostname = true; 51 ProtectSystem = "strict"; 52 ProtectControlGroups = true; 53 ProtectKernelLogs = true; 54 ProtectKernelModules = true; 55 ProtectKernelTunables = true; 56 ProtectProc = "invisible"; 57 ProcSubset = "pid"; 58 RestrictAddressFamilies = [ 59 "AF_UNIX" 60 "AF_INET" 61 "AF_INET6" 62 ]; 63 RestrictNamespaces = true; 64 RestrictRealtime = true; 65 RestrictSUIDSGID = true; 66 SystemCallArchitectures = "native"; 67 SystemCallFilter = [ 68 "@system-service" 69 "~@privileged @setuid @keyring" 70 ]; 71 UMask = "0066"; 72 }; 73 74in 75{ 76 options.services.froide-govplan = { 77 78 enable = lib.mkEnableOption "Gouvernment planer web app Govplan"; 79 80 package = lib.mkPackageOption pkgs "froide-govplan" { }; 81 82 hostName = lib.mkOption { 83 type = lib.types.str; 84 default = "localhost"; 85 description = "FQDN for the froide-govplan instance."; 86 }; 87 88 dataDir = lib.mkOption { 89 type = lib.types.str; 90 default = "/var/lib/froide-govplan"; 91 description = "Directory to store the Froide-Govplan server data."; 92 }; 93 94 secretKeyFile = lib.mkOption { 95 type = lib.types.nullOr lib.types.path; 96 default = null; 97 description = '' 98 Path to a file containing the secret key. 99 ''; 100 }; 101 102 settings = lib.mkOption { 103 description = '' 104 Configuration options to set in `extra_settings.py`. 105 ''; 106 107 default = { }; 108 109 type = lib.types.submodule { 110 freeformType = pythonFmt.type; 111 112 options = { 113 ALLOWED_HOSTS = lib.mkOption { 114 type = with lib.types; listOf str; 115 default = [ "*" ]; 116 description = '' 117 A list of valid fully-qualified domain names (FQDNs) and/or IP 118 addresses that can be used to reach the Froide-Govplan service. 119 ''; 120 }; 121 }; 122 }; 123 }; 124 125 }; 126 127 config = lib.mkIf cfg.enable { 128 129 services.froide-govplan = { 130 settings = { 131 STATIC_ROOT = "${cfg.dataDir}/static"; 132 DEBUG = false; 133 DATABASES.default = { 134 ENGINE = "django.contrib.gis.db.backends.postgis"; 135 NAME = "govplan"; 136 USER = "govplan"; 137 HOST = "/run/postgresql"; 138 }; 139 }; 140 }; 141 142 services.postgresql = { 143 enable = true; 144 ensureDatabases = [ "govplan" ]; 145 ensureUsers = [ 146 { 147 name = "govplan"; 148 ensureDBOwnership = true; 149 } 150 ]; 151 extensions = ps: with ps; [ postgis ]; 152 }; 153 154 services.nginx = { 155 enable = lib.mkDefault true; 156 virtualHosts."${cfg.hostName}".locations = { 157 "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;"; 158 "/static/".alias = "${cfg.dataDir}/static/"; 159 }; 160 proxyTimeout = lib.mkDefault "120s"; 161 }; 162 163 systemd = { 164 services = { 165 166 postgresql.serviceConfig.ExecStartPost = 167 let 168 sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" '' 169 CREATE EXTENSION IF NOT EXISTS postgis; 170 ''; 171 in 172 [ 173 '' 174 ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}" 175 '' 176 ]; 177 178 froide-govplan = { 179 description = "Gouvernment planer Govplan"; 180 serviceConfig = defaultServiceConfig // { 181 WorkingDirectory = cfg.dataDir; 182 StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan"; 183 User = "govplan"; 184 Group = "govplan"; 185 }; 186 after = [ 187 "postgresql.service" 188 "network.target" 189 "systemd-tmpfiles-setup.service" 190 ]; 191 wantedBy = [ "multi-user.target" ]; 192 environment = 193 { 194 PYTHONPATH = pkg.pythonPath; 195 GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so"; 196 GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so"; 197 } 198 // lib.optionalAttrs (cfg.secretKeyFile != null) { 199 SECRET_KEY_FILE = cfg.secretKeyFile; 200 }; 201 preStart = '' 202 # Auto-migrate on first run or if the package has changed 203 versionFile="${cfg.dataDir}/src-version" 204 version=$(cat "$versionFile" 2>/dev/null || echo 0) 205 206 if [[ $version != ${pkg.version} ]]; then 207 ${lib.getExe pkg} migrate --no-input 208 ${lib.getExe pkg} collectstatic --no-input --clear 209 echo ${pkg.version} > "$versionFile" 210 fi 211 ''; 212 script = '' 213 ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \ 214 --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \ 215 project.asgi:application 216 ''; 217 }; 218 }; 219 220 }; 221 222 systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ]; 223 224 environment.systemPackages = [ froide-govplan ]; 225 226 users.users.govplan = { 227 home = "${cfg.dataDir}"; 228 isSystemUser = true; 229 group = "govplan"; 230 }; 231 users.groups.govplan = { }; 232 233 }; 234 235 meta.maintainers = with lib.maintainers; [ onny ]; 236 237}