at master 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 } 340 // cfg.poolConfig; 341 }; 342 systemd.services.phpfpm-limesurvey.serviceConfig = { 343 ExecStartPre = pkgs.writeShellScript "limesurvey-phpfpm-exec-pre" '' 344 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_key "${stateDir}/credentials/encryption_key" 345 chown ${user}:${group} "${stateDir}/credentials/encryption_key" 346 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_nonce "${stateDir}/credentials/encryption_nonce" 347 chown ${user}:${group} "${stateDir}/credentials/encryption_nonce" 348 ''; 349 LoadCredential = [ 350 "encryption_key:${ 351 if cfg.encryptionKeyFile != null then 352 cfg.encryptionKeyFile 353 else 354 pkgs.writeText "key" cfg.encryptionKey 355 }" 356 "encryption_nonce:${ 357 if cfg.encryptionNonceFile != null then 358 cfg.encryptionNonceFile 359 else 360 pkgs.writeText "nonce" cfg.encryptionKey 361 }" 362 ]; 363 }; 364 365 services.httpd = { 366 enable = true; 367 adminAddr = mkDefault cfg.virtualHost.adminAddr; 368 extraModules = [ "proxy_fcgi" ]; 369 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ 370 cfg.virtualHost 371 { 372 documentRoot = mkForce "${cfg.package}/share/limesurvey"; 373 extraConfig = '' 374 Alias "/tmp" "${stateDir}/tmp" 375 <Directory "${stateDir}"> 376 AllowOverride all 377 Require all granted 378 Options -Indexes +FollowSymlinks 379 </Directory> 380 381 Alias "/upload" "${stateDir}/upload" 382 <Directory "${stateDir}/upload"> 383 AllowOverride all 384 Require all granted 385 Options -Indexes 386 </Directory> 387 388 <Directory "${cfg.package}/share/limesurvey"> 389 <FilesMatch "\.php$"> 390 <If "-f %{REQUEST_FILENAME}"> 391 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/" 392 </If> 393 </FilesMatch> 394 395 AllowOverride all 396 Options -Indexes 397 DirectoryIndex index.php 398 </Directory> 399 ''; 400 } 401 ]; 402 }; 403 404 systemd.tmpfiles.rules = [ 405 "d ${stateDir} 0750 ${user} ${group} - -" 406 "d ${stateDir}/tmp 0750 ${user} ${group} - -" 407 "d ${stateDir}/tmp/assets 0750 ${user} ${group} - -" 408 "d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -" 409 "d ${stateDir}/tmp/upload 0750 ${user} ${group} - -" 410 "d ${stateDir}/credentials 0700 ${user} ${group} - -" 411 "C ${stateDir}/upload 0750 ${user} ${group} - ${cfg.package}/share/limesurvey/upload" 412 ]; 413 414 systemd.services.limesurvey-init = { 415 wantedBy = [ "multi-user.target" ]; 416 before = [ "phpfpm-limesurvey.service" ]; 417 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target"; 418 environment.DBENGINE = "${cfg.database.dbEngine}"; 419 environment.LIMESURVEY_CONFIG = limesurveyConfig; 420 script = '' 421 # update or install the database as required 422 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \ 423 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose 424 ''; 425 serviceConfig = { 426 User = user; 427 Group = group; 428 Type = "oneshot"; 429 LoadCredential = [ 430 "encryption_key:${ 431 if cfg.encryptionKeyFile != null then 432 cfg.encryptionKeyFile 433 else 434 pkgs.writeText "key" cfg.encryptionKey 435 }" 436 "encryption_nonce:${ 437 if cfg.encryptionNonceFile != null then 438 cfg.encryptionNonceFile 439 else 440 pkgs.writeText "nonce" cfg.encryptionKey 441 }" 442 ]; 443 }; 444 }; 445 446 systemd.services.httpd.after = 447 optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.target"; 448 449 users.users.${user} = { 450 group = group; 451 isSystemUser = true; 452 }; 453 454 }; 455}