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