at 23.05-pre 12 kB view raw
1{ config, pkgs, lib, ... }: 2let 3 inherit (lib) any boolToString concatStringsSep isBool isString literalExpression mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalAttrs types; 4 5 package = pkgs.dolibarr.override { inherit (cfg) stateDir; }; 6 7 cfg = config.services.dolibarr; 8 vhostCfg = 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 isNull v 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; 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 { 187 188 assertions = [ 189 { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; 190 message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned"; 191 } 192 ]; 193 194 services.dolibarr.settings = { 195 dolibarr_main_url_root = "https://${cfg.domain}"; 196 dolibarr_main_document_root = "${package}/htdocs"; 197 dolibarr_main_url_root_alt = "/custom"; 198 dolibarr_main_data_root = "${cfg.stateDir}/documents"; 199 200 dolibarr_main_db_host = cfg.database.host; 201 dolibarr_main_db_port = toString cfg.database.port; 202 dolibarr_main_db_name = cfg.database.name; 203 dolibarr_main_db_prefix = "llx_"; 204 dolibarr_main_db_user = cfg.database.user; 205 dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) '' 206 file_get_contents("${cfg.database.passwordFile}") 207 ''; 208 dolibarr_main_db_type = "mysqli"; 209 dolibarr_main_db_character_set = mkDefault "utf8"; 210 dolibarr_main_db_collation = mkDefault "utf8_unicode_ci"; 211 212 # Authentication settings 213 dolibarr_main_authentication = mkDefault "dolibarr"; 214 215 # Security settings 216 dolibarr_main_prod = true; 217 dolibarr_main_force_https = vhostCfg.forceSSL; 218 dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql"; 219 dolibarr_nocsrfcheck = false; 220 dolibarr_main_instance_unique_id = '' 221 file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id") 222 ''; 223 dolibarr_mailing_limit_sendbyweb = false; 224 }; 225 226 systemd.tmpfiles.rules = [ 227 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}" 228 "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}" 229 "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}" 230 "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}" 231 ]; 232 233 services.mysql = mkIf cfg.database.createLocally { 234 enable = mkDefault true; 235 package = mkDefault pkgs.mariadb; 236 ensureDatabases = [ cfg.database.name ]; 237 ensureUsers = [ 238 { name = cfg.database.user; 239 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 240 } 241 ]; 242 }; 243 244 services.nginx.enable = mkIf (cfg.nginx != null) true; 245 services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [ 246 cfg.nginx 247 ({ 248 root = lib.mkForce "${package}/htdocs"; 249 locations."/".index = "index.php"; 250 locations."~ [^/]\\.php(/|$)" = { 251 extraConfig = '' 252 fastcgi_split_path_info ^(.+?\.php)(/.*)$; 253 fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket}; 254 ''; 255 }; 256 }) 257 ]); 258 259 systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ]; 260 services.phpfpm.pools.dolibarr = { 261 inherit (cfg) user group; 262 phpPackage = pkgs.php.buildEnv { 263 extensions = { enabled, all }: enabled ++ [ all.calendar ]; 264 # recommended by dolibarr web application 265 extraConfig = '' 266 session.use_strict_mode = 1 267 session.cookie_samesite = "Lax" 268 ; open_basedir = "${package}/htdocs, ${cfg.stateDir}" 269 allow_url_fopen = 0 270 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" 271 ''; 272 }; 273 274 settings = { 275 "listen.mode" = "0660"; 276 "listen.owner" = cfg.user; 277 "listen.group" = cfg.group; 278 } // cfg.poolConfig; 279 }; 280 281 # there are several challenges with dolibarr and NixOS which we can address here 282 # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php 283 # - the dolibarr installer requires write access to its config file during installation, though not afterwards 284 # - 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 285 systemd.services.dolibarr-config = { 286 description = "dolibarr configuration file management via NixOS"; 287 wantedBy = [ "multi-user.target" ]; 288 289 script = '' 290 # 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 291 ${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);" 292 293 # replace configuration file generated by installer with the NixOS generated configuration file 294 install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php' 295 ''; 296 297 serviceConfig = { 298 Type = "oneshot"; 299 User = cfg.user; 300 Group = cfg.group; 301 RemainAfterExit = "yes"; 302 }; 303 304 unitConfig = { 305 ConditionFileNotEmpty = "${cfg.stateDir}/conf.php"; 306 }; 307 }; 308 309 users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) { 310 isSystemUser = true; 311 group = cfg.group; 312 }; 313 314 users.groups = optionalAttrs (cfg.group == "dolibarr") { 315 dolibarr = { }; 316 }; 317 318 users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ]; 319 }; 320}