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