at 24.11-pre 12 kB view raw
1{ config, pkgs, lib, ... }: 2let 3 inherit (lib) any boolToString concatStringsSep isBool isString mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption optionalAttrs types mkPackageOption; 4 5 package = cfg.package.override { inherit (cfg) stateDir; }; 6 7 cfg = config.services.dolibarr; 8 vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}"; 9 10 mkConfigFile = filename: settings: 11 let 12 # hack in special logic for secrets so we read them from a separate file avoiding the nix store 13 secretKeys = [ "force_install_databasepass" "dolibarr_main_db_pass" "dolibarr_main_instance_unique_id" ]; 14 15 toStr = k: v: 16 if (any (str: k == str) secretKeys) then v 17 else if isString v then "'${v}'" 18 else if isBool v then boolToString v 19 else if v == null then "null" 20 else toString v 21 ; 22 in 23 pkgs.writeText filename '' 24 <?php 25 ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)} 26 ''; 27 28 # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values 29 install = { 30 force_install_noedit = 2; 31 force_install_main_data_root = "${cfg.stateDir}/documents"; 32 force_install_nophpinfo = true; 33 force_install_lockinstall = "444"; 34 force_install_distrib = "nixos"; 35 force_install_type = "mysqli"; 36 force_install_dbserver = cfg.database.host; 37 force_install_port = toString cfg.database.port; 38 force_install_database = cfg.database.name; 39 force_install_databaselogin = cfg.database.user; 40 41 force_install_mainforcehttps = vhostCfg.forceSSL or false; 42 force_install_createuser = false; 43 force_install_dolibarrlogin = null; 44 } // optionalAttrs (cfg.database.passwordFile != null) { 45 force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")''; 46 }; 47in 48{ 49 # interface 50 options.services.dolibarr = { 51 enable = mkEnableOption "dolibarr"; 52 53 package = mkPackageOption pkgs "dolibarr" { }; 54 55 domain = mkOption { 56 type = types.str; 57 default = "localhost"; 58 description = '' 59 Domain name of your server. 60 ''; 61 }; 62 63 user = mkOption { 64 type = types.str; 65 default = "dolibarr"; 66 description = '' 67 User account under which dolibarr runs. 68 69 ::: {.note} 70 If left as the default value this user will automatically be created 71 on system activation, otherwise you are responsible for 72 ensuring the user exists before the dolibarr application starts. 73 ::: 74 ''; 75 }; 76 77 group = mkOption { 78 type = types.str; 79 default = "dolibarr"; 80 description = '' 81 Group account under which dolibarr runs. 82 83 ::: {.note} 84 If left as the default value this group will automatically be created 85 on system activation, otherwise you are responsible for 86 ensuring the group exists before the dolibarr application starts. 87 ::: 88 ''; 89 }; 90 91 stateDir = mkOption { 92 type = types.str; 93 default = "/var/lib/dolibarr"; 94 description = '' 95 State and configuration directory dolibarr will use. 96 ''; 97 }; 98 99 database = { 100 host = mkOption { 101 type = types.str; 102 default = "localhost"; 103 description = "Database host address."; 104 }; 105 port = mkOption { 106 type = types.port; 107 default = 3306; 108 description = "Database host port."; 109 }; 110 name = mkOption { 111 type = types.str; 112 default = "dolibarr"; 113 description = "Database name."; 114 }; 115 user = mkOption { 116 type = types.str; 117 default = "dolibarr"; 118 description = "Database username."; 119 }; 120 passwordFile = mkOption { 121 type = with types; nullOr path; 122 default = null; 123 example = "/run/keys/dolibarr-dbpassword"; 124 description = "Database password file."; 125 }; 126 createLocally = mkOption { 127 type = types.bool; 128 default = true; 129 description = "Create the database and database user locally."; 130 }; 131 }; 132 133 settings = mkOption { 134 type = with types; (attrsOf (oneOf [ bool int str ])); 135 default = { }; 136 description = "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details."; 137 }; 138 139 nginx = mkOption { 140 type = types.nullOr (types.submodule ( 141 lib.recursiveUpdate 142 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) 143 { 144 # enable encryption by default, 145 # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text. 146 options.forceSSL.default = true; 147 options.enableACME.default = true; 148 } 149 )); 150 default = null; 151 example = lib.literalExpression '' 152 { 153 serverAliases = [ 154 "dolibarr.''${config.networking.domain}" 155 "erp.''${config.networking.domain}" 156 ]; 157 enableACME = false; 158 } 159 ''; 160 description = '' 161 With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr. 162 Set to {} if you do not need any customization to the virtual host. 163 If enabled, then by default, the {option}`serverName` is 164 `''${domain}`, 165 SSL is active, and certificates are acquired via ACME. 166 If this is set to null (the default), no nginx virtualHost will be configured. 167 ''; 168 }; 169 170 poolConfig = mkOption { 171 type = with types; attrsOf (oneOf [ str int bool ]); 172 default = { 173 "pm" = "dynamic"; 174 "pm.max_children" = 32; 175 "pm.start_servers" = 2; 176 "pm.min_spare_servers" = 2; 177 "pm.max_spare_servers" = 4; 178 "pm.max_requests" = 500; 179 }; 180 description = '' 181 Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php) 182 for details on configuration directives. 183 ''; 184 }; 185 }; 186 187 # implementation 188 config = mkIf cfg.enable (mkMerge [ 189 { 190 191 assertions = [ 192 { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; 193 message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned"; 194 } 195 ]; 196 197 services.dolibarr.settings = { 198 dolibarr_main_url_root = "https://${cfg.domain}"; 199 dolibarr_main_document_root = "${package}/htdocs"; 200 dolibarr_main_url_root_alt = "/custom"; 201 dolibarr_main_data_root = "${cfg.stateDir}/documents"; 202 203 dolibarr_main_db_host = cfg.database.host; 204 dolibarr_main_db_port = toString cfg.database.port; 205 dolibarr_main_db_name = cfg.database.name; 206 dolibarr_main_db_prefix = "llx_"; 207 dolibarr_main_db_user = cfg.database.user; 208 dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) '' 209 file_get_contents("${cfg.database.passwordFile}") 210 ''; 211 dolibarr_main_db_type = "mysqli"; 212 dolibarr_main_db_character_set = mkDefault "utf8"; 213 dolibarr_main_db_collation = mkDefault "utf8_unicode_ci"; 214 215 # Authentication settings 216 dolibarr_main_authentication = mkDefault "dolibarr"; 217 218 # Security settings 219 dolibarr_main_prod = true; 220 dolibarr_main_force_https = vhostCfg.forceSSL or false; 221 dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql"; 222 dolibarr_nocsrfcheck = false; 223 dolibarr_main_instance_unique_id = '' 224 file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id") 225 ''; 226 dolibarr_mailing_limit_sendbyweb = false; 227 }; 228 229 systemd.tmpfiles.rules = [ 230 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}" 231 "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}" 232 "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}" 233 "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}" 234 ]; 235 236 services.mysql = mkIf cfg.database.createLocally { 237 enable = mkDefault true; 238 package = mkDefault pkgs.mariadb; 239 ensureDatabases = [ cfg.database.name ]; 240 ensureUsers = [ 241 { name = cfg.database.user; 242 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 243 } 244 ]; 245 }; 246 247 services.nginx.enable = mkIf (cfg.nginx != null) true; 248 services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [ 249 cfg.nginx 250 ({ 251 root = lib.mkForce "${package}/htdocs"; 252 locations."/".index = "index.php"; 253 locations."~ [^/]\\.php(/|$)" = { 254 extraConfig = '' 255 fastcgi_split_path_info ^(.+?\.php)(/.*)$; 256 fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket}; 257 ''; 258 }; 259 }) 260 ]); 261 262 systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ]; 263 services.phpfpm.pools.dolibarr = { 264 inherit (cfg) user group; 265 phpPackage = pkgs.php.buildEnv { 266 extensions = { enabled, all }: enabled ++ [ all.calendar ]; 267 # recommended by dolibarr web application 268 extraConfig = '' 269 session.use_strict_mode = 1 270 session.cookie_samesite = "Lax" 271 ; open_basedir = "${package}/htdocs, ${cfg.stateDir}" 272 allow_url_fopen = 0 273 disable_functions = "pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals" 274 ''; 275 }; 276 277 settings = { 278 "listen.mode" = "0660"; 279 "listen.owner" = cfg.user; 280 "listen.group" = cfg.group; 281 } // cfg.poolConfig; 282 }; 283 284 # there are several challenges with dolibarr and NixOS which we can address here 285 # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php 286 # - the dolibarr installer requires write access to its config file during installation, though not afterwards 287 # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file 288 systemd.services.dolibarr-config = { 289 description = "dolibarr configuration file management via NixOS"; 290 wantedBy = [ "multi-user.target" ]; 291 292 script = '' 293 # extract the 'main instance unique id' secret that the dolibarr installer generated for us, store it in a file for use by our own NixOS generated configuration file 294 ${pkgs.php}/bin/php -r "include '${cfg.stateDir}/conf.php'; file_put_contents('${cfg.stateDir}/dolibarr_main_instance_unique_id', \$dolibarr_main_instance_unique_id);" 295 296 # replace configuration file generated by installer with the NixOS generated configuration file 297 install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php' 298 ''; 299 300 serviceConfig = { 301 Type = "oneshot"; 302 User = cfg.user; 303 Group = cfg.group; 304 RemainAfterExit = "yes"; 305 }; 306 307 unitConfig = { 308 ConditionFileNotEmpty = "${cfg.stateDir}/conf.php"; 309 }; 310 }; 311 312 users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) { 313 isSystemUser = true; 314 group = cfg.group; 315 }; 316 317 users.groups = optionalAttrs (cfg.group == "dolibarr") { 318 dolibarr = { }; 319 }; 320 } 321 (mkIf (cfg.nginx != null) { 322 users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ]; 323 }) 324]); 325}