vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / monica.nix
blob6774e2c9bb4678ca5654f8ac46ebf4f91fc7770e
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 with lib; let
8   cfg = config.services.monica;
9   monica = pkgs.monica.override {
10     dataDir = cfg.dataDir;
11   };
12   db = cfg.database;
13   mail = cfg.mail;
15   user = cfg.user;
16   group = cfg.group;
18   # shell script for local administration
19   artisan = pkgs.writeScriptBin "monica" ''
20     #! ${pkgs.runtimeShell}
21     cd ${monica}
22     sudo() {
23       if [[ "$USER" != ${user} ]]; then
24         exec /run/wrappers/bin/sudo -u ${user} "$@"
25       else
26         exec "$@"
27       fi
28     }
29     sudo ${pkgs.php}/bin/php artisan "$@"
30   '';
32   tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
33 in {
34   options.services.monica = {
35     enable = mkEnableOption "monica";
37     user = mkOption {
38       default = "monica";
39       description = "User monica runs as.";
40       type = types.str;
41     };
43     group = mkOption {
44       default = "monica";
45       description = "Group monica runs as.";
46       type = types.str;
47     };
49     appKeyFile = mkOption {
50       description = ''
51         A file containing the Laravel APP_KEY - a 32 character long,
52         base64 encoded key used for encryption where needed. Can be
53         generated with <code>head -c 32 /dev/urandom | base64</code>.
54       '';
55       example = "/run/keys/monica-appkey";
56       type = types.path;
57     };
59     hostname = lib.mkOption {
60       type = lib.types.str;
61       default =
62         if config.networking.domain != null
63         then config.networking.fqdn
64         else config.networking.hostName;
65       defaultText = lib.literalExpression "config.networking.fqdn";
66       example = "monica.example.com";
67       description = ''
68         The hostname to serve monica on.
69       '';
70     };
72     appURL = mkOption {
73       description = ''
74         The root URL that you want to host monica on. All URLs in monica will be generated using this value.
75         If you change this in the future you may need to run a command to update stored URLs in the database.
76         Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code>
77       '';
78       default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
79       defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}'';
80       example = "https://example.com";
81       type = types.str;
82     };
84     dataDir = mkOption {
85       description = "monica data directory";
86       default = "/var/lib/monica";
87       type = types.path;
88     };
90     database = {
91       host = mkOption {
92         type = types.str;
93         default = "localhost";
94         description = "Database host address.";
95       };
96       port = mkOption {
97         type = types.port;
98         default = 3306;
99         description = "Database host port.";
100       };
101       name = mkOption {
102         type = types.str;
103         default = "monica";
104         description = "Database name.";
105       };
106       user = mkOption {
107         type = types.str;
108         default = user;
109         defaultText = lib.literalExpression "user";
110         description = "Database username.";
111       };
112       passwordFile = mkOption {
113         type = with types; nullOr path;
114         default = null;
115         example = "/run/keys/monica-dbpassword";
116         description = ''
117           A file containing the password corresponding to
118           <option>database.user</option>.
119         '';
120       };
121       createLocally = mkOption {
122         type = types.bool;
123         default = true;
124         description = "Create the database and database user locally.";
125       };
126     };
128     mail = {
129       driver = mkOption {
130         type = types.enum ["smtp" "sendmail"];
131         default = "smtp";
132         description = "Mail driver to use.";
133       };
134       host = mkOption {
135         type = types.str;
136         default = "localhost";
137         description = "Mail host address.";
138       };
139       port = mkOption {
140         type = types.port;
141         default = 1025;
142         description = "Mail host port.";
143       };
144       fromName = mkOption {
145         type = types.str;
146         default = "monica";
147         description = "Mail \"from\" name.";
148       };
149       from = mkOption {
150         type = types.str;
151         default = "mail@monica.com";
152         description = "Mail \"from\" email.";
153       };
154       user = mkOption {
155         type = with types; nullOr str;
156         default = null;
157         example = "monica";
158         description = "Mail username.";
159       };
160       passwordFile = mkOption {
161         type = with types; nullOr path;
162         default = null;
163         example = "/run/keys/monica-mailpassword";
164         description = ''
165           A file containing the password corresponding to
166           <option>mail.user</option>.
167         '';
168       };
169       encryption = mkOption {
170         type = with types; nullOr (enum ["tls"]);
171         default = null;
172         description = "SMTP encryption mechanism to use.";
173       };
174     };
176     maxUploadSize = mkOption {
177       type = types.str;
178       default = "18M";
179       example = "1G";
180       description = "The maximum size for uploads (e.g. images).";
181     };
183     poolConfig = mkOption {
184       type = with types; attrsOf (oneOf [str int bool]);
185       default = {
186         "pm" = "dynamic";
187         "pm.max_children" = 32;
188         "pm.start_servers" = 2;
189         "pm.min_spare_servers" = 2;
190         "pm.max_spare_servers" = 4;
191         "pm.max_requests" = 500;
192       };
193       description = ''
194         Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal>
195         for details on configuration directives.
196       '';
197     };
199     nginx = mkOption {
200       type = types.submodule (
201         recursiveUpdate
202         (import ../web-servers/nginx/vhost-options.nix {inherit config lib;}) {}
203       );
204       default = {};
205       example = ''
206         {
207           serverAliases = [
208             "monica.''${config.networking.domain}"
209           ];
210           # To enable encryption and let let's encrypt take care of certificate
211           forceSSL = true;
212           enableACME = true;
213         }
214       '';
215       description = ''
216         With this option, you can customize the nginx virtualHost settings.
217       '';
218     };
220     config = mkOption {
221       type = with types;
222         attrsOf
223         (nullOr
224           (either
225             (oneOf [
226               bool
227               int
228               port
229               path
230               str
231             ])
232             (submodule {
233               options = {
234                 _secret = mkOption {
235                   type = nullOr str;
236                   description = ''
237                     The path to a file containing the value the
238                     option should be set to in the final
239                     configuration file.
240                   '';
241                 };
242               };
243             })));
244       default = {};
245       example = ''
246         {
247           ALLOWED_IFRAME_HOSTS = "https://example.com";
248           WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
249           AUTH_METHOD = "oidc";
250           OIDC_NAME = "MyLogin";
251           OIDC_DISPLAY_NAME_CLAIMS = "name";
252           OIDC_CLIENT_ID = "monica";
253           OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
254           OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
255           OIDC_ISSUER_DISCOVER = true;
256         }
257       '';
258       description = ''
259         monica configuration options to set in the
260         <filename>.env</filename> file.
262         Refer to <link xlink:href="https://github.com/monicahq/monica"/>
263         for details on supported values.
265         Settings containing secret data should be set to an attribute
266         set containing the attribute <literal>_secret</literal> - a
267         string pointing to a file containing the value the option
268         should be set to. See the example to get a better picture of
269         this: in the resulting <filename>.env</filename> file, the
270         <literal>OIDC_CLIENT_SECRET</literal> key will be set to the
271         contents of the <filename>/run/keys/oidc_secret</filename>
272         file.
273       '';
274     };
275   };
277   config = mkIf cfg.enable {
278     assertions = [
279       {
280         assertion = db.createLocally -> db.user == user;
281         message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true.";
282       }
283       {
284         assertion = db.createLocally -> db.passwordFile == null;
285         message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true.";
286       }
287     ];
289     services.monica.config = {
290       APP_ENV = "production";
291       APP_KEY._secret = cfg.appKeyFile;
292       APP_URL = cfg.appURL;
293       DB_HOST = db.host;
294       DB_PORT = db.port;
295       DB_DATABASE = db.name;
296       DB_USERNAME = db.user;
297       MAIL_DRIVER = mail.driver;
298       MAIL_FROM_NAME = mail.fromName;
299       MAIL_FROM = mail.from;
300       MAIL_HOST = mail.host;
301       MAIL_PORT = mail.port;
302       MAIL_USERNAME = mail.user;
303       MAIL_ENCRYPTION = mail.encryption;
304       DB_PASSWORD._secret = db.passwordFile;
305       MAIL_PASSWORD._secret = mail.passwordFile;
306       APP_SERVICES_CACHE = "/run/monica/cache/services.php";
307       APP_PACKAGES_CACHE = "/run/monica/cache/packages.php";
308       APP_CONFIG_CACHE = "/run/monica/cache/config.php";
309       APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php";
310       APP_EVENTS_CACHE = "/run/monica/cache/events.php";
311       SESSION_SECURE_COOKIE = tlsEnabled;
312     };
314     environment.systemPackages = [artisan];
316     services.mysql = mkIf db.createLocally {
317       enable = true;
318       package = mkDefault pkgs.mariadb;
319       ensureDatabases = [db.name];
320       ensureUsers = [
321         {
322           name = db.user;
323           ensurePermissions = {"${db.name}.*" = "ALL PRIVILEGES";};
324         }
325       ];
326     };
328     services.phpfpm.pools.monica = {
329       inherit user group;
330       phpOptions = ''
331         log_errors = on
332         post_max_size = ${cfg.maxUploadSize}
333         upload_max_filesize = ${cfg.maxUploadSize}
334       '';
335       settings = {
336         "listen.mode" = "0660";
337         "listen.owner" = user;
338         "listen.group" = group;
339       } // cfg.poolConfig;
340     };
342     services.nginx = {
343       enable = mkDefault true;
344       recommendedTlsSettings = true;
345       recommendedOptimisation = true;
346       recommendedGzipSettings = true;
347       recommendedBrotliSettings = true;
348       recommendedProxySettings = true;
349       virtualHosts.${cfg.hostname} = mkMerge [
350         cfg.nginx
351         {
352           root = mkForce "${monica}/public";
353           locations = {
354             "/" = {
355               index = "index.php";
356               tryFiles = "$uri $uri/ /index.php?$query_string";
357             };
358             "~ \.php$".extraConfig = ''
359               fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket};
360             '';
361             "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
362               extraConfig = "expires 365d;";
363             };
364           };
365         }
366       ];
367     };
369     systemd.services.monica-setup = {
370       description = "Preparation tasks for monica";
371       before = ["phpfpm-monica.service"];
372       after = optional db.createLocally "mysql.service";
373       wantedBy = ["multi-user.target"];
374       serviceConfig = {
375         Type = "oneshot";
376         RemainAfterExit = true;
377         User = user;
378         UMask = 077;
379         WorkingDirectory = "${monica}";
380         RuntimeDirectory = "monica/cache";
381         RuntimeDirectoryMode = 0700;
382       };
383       path = [pkgs.replace-secret];
384       script = let
385         isSecret = v: isAttrs v && v ? _secret && isString v._secret;
386         monicaEnvVars = lib.generators.toKeyValue {
387           mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
388             mkValueString = v:
389               with builtins;
390                 if isInt v
391                 then toString v
392                 else if isString v
393                 then v
394                 else if true == v
395                 then "true"
396                 else if false == v
397                 then "false"
398                 else if isSecret v
399                 then hashString "sha256" v._secret
400                 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
401           };
402         };
403         secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
404         mkSecretReplacement = file: ''
405           replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]}
406         '';
407         secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
408         filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config;
409         monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig);
410       in ''
411         # error handling
412         set -euo pipefail
414         # create .env file
415         install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env"
416         ${secretReplacements}
417         if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
418           sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
419         fi
421         # migrate & seed db
422         ${pkgs.php}/bin/php artisan key:generate --force
423         ${pkgs.php}/bin/php artisan setup:production -v --force
424       '';
425     };
427     systemd.services.monica-scheduler = {
428       description = "Background tasks for monica";
429       startAt = "minutely";
430       after = ["monica-setup.service"];
431       serviceConfig = {
432         Type = "oneshot";
433         User = user;
434         WorkingDirectory = "${monica}";
435         ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v";
436       };
437     };
439     systemd.tmpfiles.rules = [
440       "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
441       "d ${cfg.dataDir}/public                     0750 ${user} ${group} - -"
442       "d ${cfg.dataDir}/public/uploads             0750 ${user} ${group} - -"
443       "d ${cfg.dataDir}/storage                    0700 ${user} ${group} - -"
444       "d ${cfg.dataDir}/storage/app                0700 ${user} ${group} - -"
445       "d ${cfg.dataDir}/storage/fonts              0700 ${user} ${group} - -"
446       "d ${cfg.dataDir}/storage/framework          0700 ${user} ${group} - -"
447       "d ${cfg.dataDir}/storage/framework/cache    0700 ${user} ${group} - -"
448       "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
449       "d ${cfg.dataDir}/storage/framework/views    0700 ${user} ${group} - -"
450       "d ${cfg.dataDir}/storage/logs               0700 ${user} ${group} - -"
451       "d ${cfg.dataDir}/storage/uploads            0700 ${user} ${group} - -"
452     ];
454     users = {
455       users = mkIf (user == "monica") {
456         monica = {
457           inherit group;
458           isSystemUser = true;
459         };
460         "${config.services.nginx.user}".extraGroups = [group];
461       };
462       groups = mkIf (group == "monica") {
463         monica = {};
464       };
465     };
466   };