at 25.11-pre 14 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 10 inherit (lib) 11 mkDefault 12 mkEnableOption 13 mkForce 14 mkIf 15 mkMerge 16 mkOption 17 mkPackageOption 18 ; 19 inherit (lib) 20 literalExpression 21 mapAttrs 22 optional 23 optionalString 24 types 25 ; 26 27 cfg = config.services.limesurvey; 28 fpm = config.services.phpfpm.pools.limesurvey; 29 30 user = "limesurvey"; 31 group = config.services.httpd.group; 32 stateDir = "/var/lib/limesurvey"; 33 34 configType = 35 with types; 36 oneOf [ 37 (attrsOf configType) 38 str 39 int 40 bool 41 ] 42 // { 43 description = "limesurvey config type (str, int, bool or attribute set thereof)"; 44 }; 45 46 limesurveyConfig = pkgs.writeText "config.php" '' 47 <?php 48 return \array_merge( 49 \json_decode('${builtins.toJSON cfg.config}', true), 50 [ 51 'config' => [ 52 'encryptionnonce' => \trim(\file_get_contents(\getenv('CREDENTIALS_DIRECTORY') . DIRECTORY_SEPARATOR . 'encryption_nonce')), 53 'encryptionsecretboxkey' => \trim(\file_get_contents(\getenv('CREDENTIALS_DIRECTORY') . DIRECTORY_SEPARATOR . 'encryption_key')), 54 ] 55 ] 56 ); 57 ?> 58 ''; 59 60 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; 61 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; 62 63in 64{ 65 # interface 66 67 options.services.limesurvey = { 68 enable = mkEnableOption "Limesurvey web application"; 69 70 package = mkPackageOption pkgs "limesurvey" { }; 71 72 encryptionKey = mkOption { 73 type = types.nullOr types.str; 74 default = null; 75 visible = false; 76 description = '' 77 This is a 32-byte key used to encrypt variables in the database. 78 You _must_ change this from the default value. 79 ''; 80 }; 81 82 encryptionNonce = mkOption { 83 type = types.nullOr types.str; 84 default = null; 85 visible = false; 86 description = '' 87 This is a 24-byte nonce used to encrypt variables in the database. 88 You _must_ change this from the default value. 89 ''; 90 }; 91 92 encryptionKeyFile = mkOption { 93 type = types.nullOr types.path; 94 default = null; 95 description = '' 96 32-byte key used to encrypt variables in the database. 97 98 Note: It should be string not a store path in order to prevent the password from being world readable 99 ''; 100 }; 101 102 encryptionNonceFile = mkOption { 103 type = types.nullOr types.path; 104 default = null; 105 description = '' 106 24-byte used to encrypt variables in the database. 107 108 Note: It should be string not a store path in order to prevent the password from being world readable 109 ''; 110 }; 111 112 database = { 113 type = mkOption { 114 type = types.enum [ 115 "mysql" 116 "pgsql" 117 "odbc" 118 "mssql" 119 ]; 120 example = "pgsql"; 121 default = "mysql"; 122 description = "Database engine to use."; 123 }; 124 125 dbEngine = mkOption { 126 type = types.enum [ 127 "MyISAM" 128 "InnoDB" 129 ]; 130 default = "InnoDB"; 131 description = "Database storage engine to use."; 132 }; 133 134 host = mkOption { 135 type = types.str; 136 default = "localhost"; 137 description = "Database host address."; 138 }; 139 140 port = mkOption { 141 type = types.port; 142 default = if cfg.database.type == "pgsql" then 5442 else 3306; 143 defaultText = literalExpression "3306"; 144 description = "Database host port."; 145 }; 146 147 name = mkOption { 148 type = types.str; 149 default = "limesurvey"; 150 description = "Database name."; 151 }; 152 153 user = mkOption { 154 type = types.str; 155 default = "limesurvey"; 156 description = "Database user."; 157 }; 158 159 passwordFile = mkOption { 160 type = types.nullOr types.path; 161 default = null; 162 example = "/run/keys/limesurvey-dbpassword"; 163 description = '' 164 A file containing the password corresponding to 165 {option}`database.user`. 166 ''; 167 }; 168 169 socket = mkOption { 170 type = types.nullOr types.path; 171 default = 172 if mysqlLocal then 173 "/run/mysqld/mysqld.sock" 174 else if pgsqlLocal then 175 "/run/postgresql" 176 else 177 null; 178 defaultText = literalExpression "/run/mysqld/mysqld.sock"; 179 description = "Path to the unix socket file to use for authentication."; 180 }; 181 182 createLocally = mkOption { 183 type = types.bool; 184 default = cfg.database.type == "mysql"; 185 defaultText = literalExpression "true"; 186 description = '' 187 Create the database and database user locally. 188 This currently only applies if database type "mysql" is selected. 189 ''; 190 }; 191 }; 192 193 virtualHost = mkOption { 194 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix); 195 example = literalExpression '' 196 { 197 hostName = "survey.example.org"; 198 adminAddr = "webmaster@example.org"; 199 forceSSL = true; 200 enableACME = true; 201 } 202 ''; 203 description = '' 204 Apache configuration can be done by adapting `services.httpd.virtualHosts.<name>`. 205 See [](#opt-services.httpd.virtualHosts) for further information. 206 ''; 207 }; 208 209 poolConfig = mkOption { 210 type = 211 with types; 212 attrsOf (oneOf [ 213 str 214 int 215 bool 216 ]); 217 default = { 218 "pm" = "dynamic"; 219 "pm.max_children" = 32; 220 "pm.start_servers" = 2; 221 "pm.min_spare_servers" = 2; 222 "pm.max_spare_servers" = 4; 223 "pm.max_requests" = 500; 224 }; 225 description = '' 226 Options for the LimeSurvey PHP pool. See the documentation on `php-fpm.conf` 227 for details on configuration directives. 228 ''; 229 }; 230 231 config = mkOption { 232 type = configType; 233 default = { }; 234 description = '' 235 LimeSurvey configuration. Refer to 236 <https://manual.limesurvey.org/Optional_settings> 237 for details on supported values. 238 ''; 239 }; 240 }; 241 242 # implementation 243 244 config = mkIf cfg.enable { 245 246 assertions = [ 247 { 248 assertion = cfg.database.createLocally -> cfg.database.type == "mysql"; 249 message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'"; 250 } 251 { 252 assertion = cfg.database.createLocally -> cfg.database.user == user; 253 message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true"; 254 } 255 { 256 assertion = cfg.database.createLocally -> cfg.database.socket != null; 257 message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true"; 258 } 259 { 260 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; 261 message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true"; 262 } 263 { 264 assertion = cfg.encryptionKey != null || cfg.encryptionKeyFile != null; 265 message = '' 266 You must set `services.limesurvey.encryptionKeyFile` to a file containing a 32-character uppercase hex string. 267 268 If this message appears when updating your system, please turn off encryption 269 in the LimeSurvey interface and create backups before filling the key. 270 ''; 271 } 272 { 273 assertion = cfg.encryptionNonce != null || cfg.encryptionNonceFile != null; 274 message = '' 275 You must set `services.limesurvey.encryptionNonceFile` to a file containing a 24-character uppercase hex string. 276 277 If this message appears when updating your system, please turn off encryption 278 in the LimeSurvey interface and create backups before filling the nonce. 279 ''; 280 } 281 ]; 282 283 services.limesurvey.config = mapAttrs (name: mkDefault) { 284 runtimePath = "${stateDir}/tmp/runtime"; 285 components = { 286 db = { 287 connectionString = 288 "${cfg.database.type}:dbname=${cfg.database.name};host=${ 289 if pgsqlLocal then cfg.database.socket else cfg.database.host 290 };port=${toString cfg.database.port}" 291 + optionalString mysqlLocal ";socket=${cfg.database.socket}"; 292 username = cfg.database.user; 293 password = mkIf ( 294 cfg.database.passwordFile != null 295 ) "file_get_contents(\"${toString cfg.database.passwordFile}\");"; 296 tablePrefix = "limesurvey_"; 297 }; 298 assetManager.basePath = "${stateDir}/tmp/assets"; 299 urlManager = { 300 urlFormat = "path"; 301 showScriptName = false; 302 }; 303 }; 304 config = { 305 tempdir = "${stateDir}/tmp"; 306 uploaddir = "${stateDir}/upload"; 307 force_ssl = mkIf ( 308 cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL 309 ) "on"; 310 config.defaultlang = "en"; 311 }; 312 }; 313 314 services.mysql = mkIf mysqlLocal { 315 enable = true; 316 package = mkDefault pkgs.mariadb; 317 ensureDatabases = [ cfg.database.name ]; 318 ensureUsers = [ 319 { 320 name = cfg.database.user; 321 ensurePermissions = { 322 "${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX"; 323 }; 324 } 325 ]; 326 }; 327 328 services.phpfpm.pools.limesurvey = { 329 inherit user group; 330 phpPackage = pkgs.php81; 331 phpEnv.DBENGINE = "${cfg.database.dbEngine}"; 332 phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}"; 333 # App code cannot access credentials directly since the service starts 334 # with the root user so we copy the credentials to a place accessible to Limesurvey 335 phpEnv.CREDENTIALS_DIRECTORY = "${stateDir}/credentials"; 336 settings = { 337 "listen.owner" = config.services.httpd.user; 338 "listen.group" = config.services.httpd.group; 339 } // cfg.poolConfig; 340 }; 341 systemd.services.phpfpm-limesurvey.serviceConfig = { 342 ExecStartPre = pkgs.writeShellScript "limesurvey-phpfpm-exec-pre" '' 343 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_key "${stateDir}/credentials/encryption_key" 344 chown ${user}:${group} "${stateDir}/credentials/encryption_key" 345 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_nonce "${stateDir}/credentials/encryption_nonce" 346 chown ${user}:${group} "${stateDir}/credentials/encryption_nonce" 347 ''; 348 LoadCredential = [ 349 "encryption_key:${ 350 if cfg.encryptionKeyFile != null then 351 cfg.encryptionKeyFile 352 else 353 pkgs.writeText "key" cfg.encryptionKey 354 }" 355 "encryption_nonce:${ 356 if cfg.encryptionNonceFile != null then 357 cfg.encryptionNonceFile 358 else 359 pkgs.writeText "nonce" cfg.encryptionKey 360 }" 361 ]; 362 }; 363 364 services.httpd = { 365 enable = true; 366 adminAddr = mkDefault cfg.virtualHost.adminAddr; 367 extraModules = [ "proxy_fcgi" ]; 368 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ 369 cfg.virtualHost 370 { 371 documentRoot = mkForce "${cfg.package}/share/limesurvey"; 372 extraConfig = '' 373 Alias "/tmp" "${stateDir}/tmp" 374 <Directory "${stateDir}"> 375 AllowOverride all 376 Require all granted 377 Options -Indexes +FollowSymlinks 378 </Directory> 379 380 Alias "/upload" "${stateDir}/upload" 381 <Directory "${stateDir}/upload"> 382 AllowOverride all 383 Require all granted 384 Options -Indexes 385 </Directory> 386 387 <Directory "${cfg.package}/share/limesurvey"> 388 <FilesMatch "\.php$"> 389 <If "-f %{REQUEST_FILENAME}"> 390 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/" 391 </If> 392 </FilesMatch> 393 394 AllowOverride all 395 Options -Indexes 396 DirectoryIndex index.php 397 </Directory> 398 ''; 399 } 400 ]; 401 }; 402 403 systemd.tmpfiles.rules = [ 404 "d ${stateDir} 0750 ${user} ${group} - -" 405 "d ${stateDir}/tmp 0750 ${user} ${group} - -" 406 "d ${stateDir}/tmp/assets 0750 ${user} ${group} - -" 407 "d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -" 408 "d ${stateDir}/tmp/upload 0750 ${user} ${group} - -" 409 "d ${stateDir}/credentials 0700 ${user} ${group} - -" 410 "C ${stateDir}/upload 0750 ${user} ${group} - ${cfg.package}/share/limesurvey/upload" 411 ]; 412 413 systemd.services.limesurvey-init = { 414 wantedBy = [ "multi-user.target" ]; 415 before = [ "phpfpm-limesurvey.service" ]; 416 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; 417 environment.DBENGINE = "${cfg.database.dbEngine}"; 418 environment.LIMESURVEY_CONFIG = limesurveyConfig; 419 script = '' 420 # update or install the database as required 421 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \ 422 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose 423 ''; 424 serviceConfig = { 425 User = user; 426 Group = group; 427 Type = "oneshot"; 428 LoadCredential = [ 429 "encryption_key:${ 430 if cfg.encryptionKeyFile != null then 431 cfg.encryptionKeyFile 432 else 433 pkgs.writeText "key" cfg.encryptionKey 434 }" 435 "encryption_nonce:${ 436 if cfg.encryptionNonceFile != null then 437 cfg.encryptionNonceFile 438 else 439 pkgs.writeText "nonce" cfg.encryptionKey 440 }" 441 ]; 442 }; 443 }; 444 445 systemd.services.httpd.after = 446 optional mysqlLocal "mysql.service" 447 ++ optional pgsqlLocal "postgresql.service"; 448 449 users.users.${user} = { 450 group = group; 451 isSystemUser = true; 452 }; 453 454 }; 455}