vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / movim.nix
blob51c3156fef063b73d617f51ad8f0daaee985bd2d
1 { config, lib, pkgs, ... }:
3 let
4   inherit (lib)
5     filterAttrsRecursive
6     generators
7     literalExpression
8     mkDefault
9     mkIf
10     mkOption
11     mkEnableOption
12     mkPackageOption
13     mkMerge
14     pipe
15     types
16     ;
18   cfg = config.services.movim;
20   defaultPHPCfg = {
21     "output_buffering" = 0;
22     "error_reporting" = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
23     "opcache.enable_cli" = 1;
24     "opcache.interned_strings_buffer" = 8;
25     "opcache.max_accelerated_files" = 6144;
26     "opcache.memory_consumption" = 128;
27     "opcache.revalidate_freq" = 2;
28     "opcache.fast_shutdown" = 1;
29   };
31   phpCfg = generators.toKeyValue
32     { mkKeyValue = generators.mkKeyValueDefault { } " = "; }
33     (defaultPHPCfg // cfg.phpCfg);
35   podConfigFlags =
36     let
37       bevalue = a: lib.escapeShellArg (generators.mkValueStringDefault { } a);
38     in
39     lib.concatStringsSep " "
40       (lib.attrsets.foldlAttrs
41         (acc: k: v: acc ++ lib.optional (v != null) "--${k}=${bevalue v}")
42         [ ]
43         cfg.podConfig);
45   package =
46     let
47       p = cfg.package.override
48         ({
49           inherit phpCfg;
50           withPgsql = cfg.database.type == "pgsql";
51           withMysql = cfg.database.type == "mysql";
52           inherit (cfg) minifyStaticFiles;
53         } // lib.optionalAttrs (lib.isAttrs cfg.minifyStaticFiles) (with cfg.minifyStaticFiles; {
54           esbuild = esbuild.package;
55           lightningcss = lightningcss.package;
56           scour = scour.package;
57         }));
58     in
59     p.overrideAttrs (finalAttrs: prevAttrs:
60       let
61         appDir = "$out/share/php/${finalAttrs.pname}";
63         stateDirectories = ''
64           # Symlinking in our state directories
65           rm -rf $out/.env $out/cache ${appDir}/public/cache
66           ln -s ${cfg.dataDir}/.env ${appDir}/.env
67           ln -s ${cfg.dataDir}/public/cache ${appDir}/public/cache
68           ln -s ${cfg.logDir} ${appDir}/log
69           ln -s ${cfg.runtimeDir}/cache ${appDir}/cache
70         '';
72         exposeComposer = ''
73           # Expose PHP Composer for scripts
74           mkdir -p $out/bin
75           echo "#!${lib.getExe pkgs.dash}" > $out/bin/movim-composer
76           echo "${finalAttrs.php.packages.composer}/bin/composer --working-dir="${appDir}" \"\$@\"" >> $out/bin/movim-composer
77           chmod +x $out/bin/movim-composer
78         '';
80         podConfigInputDisableReplace = lib.optionalString (podConfigFlags != "")
81           (lib.concatStringsSep "\n"
82             (lib.attrsets.foldlAttrs
83               (acc: k: v:
84                 acc ++ lib.optional (v != null)
85                   # Disable all Admin panel options that were set in the
86                   # `cfg.podConfig` to prevent confusing situtions where the
87                   # values are rewritten on server reboot
88                   ''
89                     substituteInPlace ${appDir}/app/Widgets/AdminMain/adminmain.tpl \
90                       --replace-warn 'name="${k}"' 'name="${k}" readonly'
91                   '')
92               [ ]
93               cfg.podConfig));
95         precompressStaticFilesJobs =
96           let
97             inherit (cfg.precompressStaticFiles) brotli gzip;
99             findTextFileNames = lib.concatStringsSep " -o "
100               (builtins.map (n: ''-iname "*.${n}"'')
101                 [ "css" "ini" "js" "json" "manifest" "mjs" "svg" "webmanifest" ]);
102           in
103           lib.concatStringsSep "\n" [
104             (lib.optionalString brotli.enable ''
105               echo -n "Precompressing static files with Brotli …"
106               find ${appDir}/public -type f ${findTextFileNames} -print0 \
107                 | xargs -0 -n 1 -P $NIX_BUILD_CORES ${pkgs.writeShellScript "movim_precompress_broti" ''
108                     file="$1"
109                     ${lib.getExe brotli.package} --keep --quality=${builtins.toString brotli.compressionLevel} --output=$file.br $file
110                   ''}
111               echo " done."
112             '')
113             (lib.optionalString gzip.enable ''
114               echo -n "Precompressing static files with Gzip …"
115               find ${appDir}/public -type f ${findTextFileNames} -print0 \
116                 | xargs -0 -n 1 -P $NIX_BUILD_CORES ${pkgs.writeShellScript "movim_precompress_broti" ''
117                     file="$1"
118                     ${lib.getExe gzip.package} -c -${builtins.toString gzip.compressionLevel} $file > $file.gz
119                   ''}
120               echo " done."
121             '')
122           ];
123       in
124       {
125         postInstall = lib.concatStringsSep "\n\n" [
126           prevAttrs.postInstall
127           stateDirectories
128           exposeComposer
129           podConfigInputDisableReplace
130           precompressStaticFilesJobs
131         ];
132       });
134   configFile = pipe cfg.settings [
135     (filterAttrsRecursive (_: v: v != null))
136     (generators.toKeyValue { })
137     (pkgs.writeText "movim-env")
138   ];
140   pool = "movim";
141   fpm = config.services.phpfpm.pools.${pool};
142   phpExecutionUnit = "phpfpm-${pool}";
144   dbService = {
145     "postgresql" = "postgresql.service";
146     "mysql" = "mysql.service";
147   }.${cfg.database.type};
150   options.services = {
151     movim = {
152       enable = mkEnableOption "a Movim instance";
153       package = mkPackageOption pkgs "movim" { };
154       phpPackage = mkPackageOption pkgs "php" { };
156       phpCfg = mkOption {
157         type = with types; attrsOf (oneOf [ int str bool ]);
158         defaultText = literalExpression (generators.toPretty { } defaultPHPCfg);
159         default = { };
160         description = "Extra PHP INI options such as `memory_limit`, `max_execution_time`, etc.";
161       };
163       user = mkOption {
164         type = types.nonEmptyStr;
165         default = "movim";
166         description = "User running Movim service";
167       };
169       group = mkOption {
170         type = types.nonEmptyStr;
171         default = "movim";
172         description = "Group running Movim service";
173       };
175       dataDir = mkOption {
176         type = types.nonEmptyStr;
177         default = "/var/lib/movim";
178         description = "State directory of the `movim` user which holds the application’s state & data.";
179       };
181       logDir = mkOption {
182         type = types.nonEmptyStr;
183         default = "/var/log/movim";
184         description = "Log directory of the `movim` user which holds the application’s logs.";
185       };
187       runtimeDir = mkOption {
188         type = types.nonEmptyStr;
189         default = "/run/movim";
190         description = "Runtime directory of the `movim` user which holds the application’s caches & temporary files.";
191       };
193       domain = mkOption {
194         type = types.nonEmptyStr;
195         description = "Fully-qualified domain name (FQDN) for the Movim instance.";
196       };
198       port = mkOption {
199         type = types.port;
200         default = 8080;
201         description = "Movim daemon port.";
202       };
204       debug = mkOption {
205         type = types.bool;
206         default = false;
207         description = "Debugging logs.";
208       };
210       verbose = mkOption {
211         type = types.bool;
212         default = false;
213         description = "Verbose logs.";
214       };
216       minifyStaticFiles = mkOption {
217         type = with types; either bool (submodule {
218           options = {
219             script = mkOption {
220               type = types.submodule {
221                 options = {
222                   enable = mkEnableOption "Script minification";
223                   package = mkPackageOption pkgs "esbuild" { };
224                   target = mkOption {
225                     type = with types; nullOr nonEmptyStr;
226                     default = null;
227                   };
228                 };
229               };
230             };
231             style = mkOption {
232               type = types.submodule {
233                 options = {
234                   enable = mkEnableOption "Script minification";
235                   package = mkPackageOption pkgs "lightningcss" { };
236                   target = mkOption {
237                     type = with types; nullOr nonEmptyStr;
238                     default = null;
239                   };
240                 };
241               };
242             };
243             svg = mkOption {
244               type = types.submodule {
245                 options = {
246                   enable = mkEnableOption "SVG minification";
247                   package = mkPackageOption pkgs "scour" { };
248                 };
249               };
250             };
251           };
252         });
253         default = true;
254         description = "Do minification on public static files";
255       };
257       precompressStaticFiles = mkOption {
258         type = with types; submodule {
259           options = {
260             brotli = {
261               enable = mkEnableOption "Brotli precompression";
262               package = mkPackageOption pkgs "brotli" { };
263               compressionLevel = mkOption {
264                 type = types.ints.between 0 11;
265                 default = 11;
266                 description = "Brotli compression level";
267               };
268             };
269             gzip = {
270               enable = mkEnableOption "Gzip precompression";
271               package = mkPackageOption pkgs "gzip" { };
272               compressionLevel = mkOption {
273                 type = types.ints.between 1 9;
274                 default = 9;
275                 description = "Gzip compression level";
276               };
277             };
278           };
279         };
280         default = {
281           brotli.enable = true;
282           gzip.enable = false;
283         };
284         description = "Aggressively precompress static files";
285       };
287       podConfig = mkOption {
288         type = types.submodule {
289           options = {
290             info = mkOption {
291               type = with types; nullOr str;
292               default = null;
293               description = "Content of the info box on the login page";
294             };
296             description = mkOption {
297               type = with types; nullOr str;
298               default = null;
299               description = "General description of the instance";
300             };
302             timezone = mkOption {
303               type = with types; nullOr str;
304               default = null;
305               description = "The server timezone";
306             };
308             restrictsuggestions = mkOption {
309               type = with types; nullOr bool;
310               default = null;
311               description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services";
312             };
314             chatonly = mkOption {
315               type = with types; nullOr bool;
316               default = null;
317               description = "Disable all the social feature (Communities, Blog…) and keep only the chat ones";
318             };
320             disableregistration = mkOption {
321               type = with types; nullOr bool;
322               default = null;
323               description = "Remove the XMPP registration flow and buttons from the interface";
324             };
326             loglevel = mkOption {
327               type = with types; nullOr (ints.between 0 3);
328               default = null;
329               description = "The server loglevel";
330             };
332             locale = mkOption {
333               type = with types; nullOr str;
334               default = null;
335               description = "The server main locale";
336             };
338             xmppdomain = mkOption {
339               type = with types; nullOr str;
340               default = null;
341               description = "The default XMPP server domain";
342             };
344             xmppdescription = mkOption {
345               type = with types; nullOr str;
346               default = null;
347               description = "The default XMPP server description";
348             };
350             xmppwhitelist = mkOption {
351               type = with types; nullOr str;
352               default = null;
353               description = "The allowlisted XMPP servers";
354             };
355           };
356         };
357         default = { };
358         description = ''
359           Pod configuration (values from `php daemon.php config --help`).
360           Note that these values will now be disabled in the admin panel.
361         '';
362       };
364       settings = mkOption {
365         type = with types; attrsOf (nullOr (oneOf [ int str bool ]));
366         default = { };
367         description = ".env settings for Movim. Secrets should use `secretFile` option instead. `null`s will be culled.";
368       };
370       secretFile = mkOption {
371         type = with types; nullOr path;
372         default = null;
373         description = "The secret file to be sourced for the .env settings.";
374       };
376       database = {
377         type = mkOption {
378           type = types.enum [ "mysql" "postgresql" ];
379           example = "mysql";
380           default = "postgresql";
381           description = "Database engine to use.";
382         };
384         name = mkOption {
385           type = types.str;
386           default = "movim";
387           description = "Database name.";
388         };
390         user = mkOption {
391           type = types.str;
392           default = "movim";
393           description = "Database username.";
394         };
396         createLocally = mkOption {
397           type = types.bool;
398           default = true;
399           description = "local database using UNIX socket authentication";
400         };
401       };
403       nginx = mkOption {
404         type = with types; nullOr (submodule
405           (import ../web-servers/nginx/vhost-options.nix {
406             inherit config lib;
407           }));
408         default = null;
409         example = lib.literalExpression /* nginx */ ''
410           {
411             serverAliases = [
412               "pics.''${config.networking.domain}"
413             ];
414             enableACME = true;
415             forceHttps = true;
416           }
417         '';
418         description = ''
419           With this option, you can customize an nginx virtual host which already has sensible defaults for Movim.
420           Set to `{ }` if you do not need any customization to the virtual host.
421           If enabled, then by default, the {option}`serverName` is `''${domain}`,
422           If this is set to null (the default), no nginx virtualHost will be configured.
423         '';
424       };
426       poolConfig = mkOption {
427         type = with types; attrsOf (oneOf [ int str bool ]);
428         default = { };
429         description = "Options for Movim’s PHP-FPM pool.";
430       };
431     };
432   };
434   config = mkIf cfg.enable {
435     environment.systemPackages = [ cfg.package ];
437     users = {
438       users = {
439         movim = mkIf (cfg.user == "movim") {
440           isSystemUser = true;
441           group = cfg.group;
442         };
443         "${config.services.nginx.user}".extraGroups = [ cfg.group ];
444       };
445       groups = {
446         ${cfg.group} = { };
447       };
448     };
450     services = {
451       movim = {
452         settings = mkMerge [
453           {
454             DAEMON_URL = "//${cfg.domain}";
455             DAEMON_PORT = cfg.port;
456             DAEMON_INTERFACE = "127.0.0.1";
457             DAEMON_DEBUG = cfg.debug;
458             DAEMON_VERBOSE = cfg.verbose;
459           }
460           (mkIf cfg.database.createLocally {
461             DB_DRIVER = {
462               "postgresql" = "pgsql";
463               "mysql" = "mysql";
464             }.${cfg.database.type};
465             DB_HOST = "localhost";
466             DB_PORT = config.services.${cfg.database.type}.settings.port;
467             DB_DATABASE = cfg.database.name;
468             DB_USERNAME = cfg.database.user;
469             DB_PASSWORD = "";
470           })
471         ];
473         poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
474           "pm" = "dynamic";
475           "php_admin_value[error_log]" = "stderr";
476           "php_admin_flag[log_errors]" = true;
477           "catch_workers_output" = true;
478           "pm.max_children" = 32;
479           "pm.start_servers" = 2;
480           "pm.min_spare_servers" = 2;
481           "pm.max_spare_servers" = 8;
482           "pm.max_requests" = 500;
483         };
484       };
486       nginx = mkIf (cfg.nginx != null) {
487         enable = true;
488         recommendedOptimisation = true;
489         recommendedGzipSettings = true;
490         recommendedBrotliSettings = true;
491         recommendedProxySettings = true;
492         # TODO: recommended cache options already in Nginx⁇
493         appendHttpConfig = /* nginx */ ''
494           fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m;
495           fastcgi_cache_key "$scheme$request_method$host$request_uri";
496         '';
497         virtualHosts."${cfg.domain}" = mkMerge [
498           cfg.nginx
499           {
500             root = lib.mkForce "${package}/share/php/movim/public";
501             locations = {
502               "/favicon.ico" = {
503                 priority = 100;
504                 extraConfig = /* nginx */ ''
505                   access_log off;
506                   log_not_found off;
507                 '';
508               };
509               "/robots.txt" = {
510                 priority = 100;
511                 extraConfig = /* nginx */ ''
512                   access_log off;
513                   log_not_found off;
514                 '';
515               };
516               "~ /\\.(?!well-known).*" = {
517                 priority = 210;
518                 extraConfig = /* nginx */ ''
519                   deny all;
520                 '';
521               };
522               # Ask nginx to cache every URL starting with "/picture"
523               "/picture" = {
524                 priority = 400;
525                 tryFiles = "$uri $uri/ /index.php$is_args$args";
526                 extraConfig = /* nginx */ ''
527                   set $no_cache 0; # Enable cache only there
528                 '';
529               };
530               "/" = {
531                 priority = 490;
532                 tryFiles = "$uri $uri/ /index.php$is_args$args";
533                 extraConfig = /* nginx */ ''
534                   # https://github.com/movim/movim/issues/314
535                   add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";
536                   set $no_cache 1;
537                 '';
538               };
539               "~ \\.php$" = {
540                 priority = 500;
541                 tryFiles = "$uri =404";
542                 extraConfig = /* nginx */ ''
543                   include ${config.services.nginx.package}/conf/fastcgi.conf;
544                   add_header X-Cache $upstream_cache_status;
545                   fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie";
546                   fastcgi_cache nginx_cache;
547                   fastcgi_cache_valid any 7d;
548                   fastcgi_cache_bypass $no_cache;
549                   fastcgi_no_cache $no_cache;
550                   fastcgi_split_path_info ^(.+\.php)(/.+)$;
551                   fastcgi_index index.php;
552                   fastcgi_pass unix:${fpm.socket};
553                 '';
554               };
555               "/ws/" = {
556                 priority = 900;
557                 proxyPass = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/";
558                 proxyWebsockets = true;
559                 recommendedProxySettings = true;
560                 extraConfig = /* nginx */ ''
561                   proxy_set_header X-Forwarded-Proto $scheme;
562                   proxy_redirect off;
563                 '';
564               };
565             };
566             extraConfig = /* ngnix */ ''
567               index index.php;
568             '';
569           }
570         ];
571       };
573       mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
574         enable = mkDefault true;
575         package = mkDefault pkgs.mariadb;
576         ensureDatabases = [ cfg.database.name ];
577         ensureUsers = [{
578           name = cfg.user;
579           ensureDBOwnership = true;
580         }];
581       };
583       postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") {
584         enable = mkDefault true;
585         ensureDatabases = [ cfg.database.name ];
586         ensureUsers = [{
587           name = cfg.user;
588           ensureDBOwnership = true;
589         }];
590         authentication = ''
591           host ${cfg.database.name} ${cfg.database.user} localhost trust
592         '';
593       };
595       phpfpm.pools.${pool} =
596         let
597           socketOwner =
598             if (cfg.nginx != null)
599             then config.services.nginx.user
600             else cfg.user;
601         in
602         {
603           phpPackage = package.php;
604           user = cfg.user;
605           group = cfg.group;
607           phpOptions = ''
608             error_log = 'stderr'
609             log_errors = on
610           '';
612           settings = {
613             "listen.owner" = socketOwner;
614             "listen.group" = cfg.group;
615             "listen.mode" = "0660";
616             "catch_workers_output" = true;
617           } // cfg.poolConfig;
618         };
619     };
621     systemd = {
622       services.movim-data-setup = {
623         description = "Movim setup: .env file, databases init, cache reload";
624         wantedBy = [ "multi-user.target" ];
625         requiredBy = [ "${phpExecutionUnit}.service" ];
626         before = [ "${phpExecutionUnit}.service" ];
627         after = lib.optional cfg.database.createLocally dbService;
628         requires = lib.optional cfg.database.createLocally dbService;
630         serviceConfig = {
631           Type = "oneshot";
632           User = cfg.user;
633           Group = cfg.group;
634           UMask = "077";
635         } // lib.optionalAttrs (cfg.secretFile != null) {
636           LoadCredential = "env-secrets:${cfg.secretFile}";
637         };
639         script = ''
640           # Env vars
641           rm -f ${cfg.dataDir}/.env
642           cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
643           echo -e '\n' >> ${cfg.dataDir}/.env
644           if [[ -f "$CREDENTIALS_DIRECTORY/env-secrets"  ]]; then
645             cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
646             echo -e '\n' >> ${cfg.dataDir}/.env
647           fi
649           # Caches, logs
650           mkdir -p ${cfg.dataDir}/public/cache ${cfg.logDir} ${cfg.runtimeDir}/cache
651           chmod -R ug+rw ${cfg.dataDir}/public/cache
652           chmod -R ug+rw ${cfg.logDir}
653           chmod -R ug+rwx ${cfg.runtimeDir}/cache
655           # Migrations
656           MOVIM_VERSION="${package.version}"
657           if [[ ! -f "${cfg.dataDir}/.migration-version" ]] || [[ "$MOVIM_VERSION" != "$(<${cfg.dataDir}/.migration-version)" ]]; then
658             ${package}/bin/movim-composer movim:migrate && echo $MOVIM_VERSION > ${cfg.dataDir}/.migration-version
659           fi
660         ''
661         + lib.optionalString (podConfigFlags != "") (
662           let
663             flags = lib.concatStringsSep " "
664               ([ "--no-interaction" ]
665                 ++ lib.optional cfg.debug "-vvv"
666                 ++ lib.optional (!cfg.debug && cfg.verbose) "-v");
667           in
668           ''
669             ${lib.getExe package} config ${podConfigFlags}
670           ''
671         );
672       };
674       services.movim = {
675         description = "Movim daemon";
676         wantedBy = [ "multi-user.target" ];
677         after = [ "movim-data-setup.service" ];
678         requires = [ "movim-data-setup.service" ]
679           ++ lib.optional cfg.database.createLocally dbService;
680         environment = {
681           PUBLIC_URL = "//${cfg.domain}";
682           WS_PORT = builtins.toString cfg.port;
683         };
685         serviceConfig = {
686           User = cfg.user;
687           Group = cfg.group;
688           WorkingDirectory = "${package}/share/php/movim";
689           ExecStart = "${lib.getExe package} start";
690         };
691       };
693       services.${phpExecutionUnit} = {
694         after = [ "movim-data-setup.service" ];
695         requires = [ "movim-data-setup.service" ]
696           ++ lib.optional cfg.database.createLocally dbService;
697       };
699       tmpfiles.settings."10-movim" = with cfg; {
700         "${dataDir}".d = { inherit user group; mode = "0710"; };
701         "${dataDir}/public".d = { inherit user group; mode = "0750"; };
702         "${dataDir}/public/cache".d = { inherit user group; mode = "0750"; };
703         "${runtimeDir}".d = { inherit user group; mode = "0700"; };
704         "${runtimeDir}/cache".d = { inherit user group; mode = "0700"; };
705         "${logDir}".d = { inherit user group; mode = "0700"; };
706       };
707     };
708   };