vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / snipe-it.nix
blob272dd23d7271cb6b7c799d9e1eff5af24572da19
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.snipe-it;
7   snipe-it = pkgs.snipe-it.override {
8     dataDir = cfg.dataDir;
9   };
10   db = cfg.database;
11   mail = cfg.mail;
13   user = cfg.user;
14   group = cfg.group;
16   tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
18   inherit (snipe-it.passthru) phpPackage;
20   # shell script for local administration
21   artisan = (pkgs.writeScriptBin "snipe-it" ''
22     #! ${pkgs.runtimeShell}
23     cd "${snipe-it}/share/php/snipe-it"
24     sudo=exec
25     if [[ "$USER" != ${user} ]]; then
26       sudo='exec /run/wrappers/bin/sudo -u ${user}'
27     fi
28     $sudo ${phpPackage}/bin/php artisan $*
29   '').overrideAttrs (old: {
30     meta = old.meta // {
31       mainProgram = "snipe-it";
32     };
33   });
34 in {
35   options.services.snipe-it = {
37     enable = mkEnableOption "snipe-it, a free open source IT asset/license management system";
39     user = mkOption {
40       default = "snipeit";
41       description = "User snipe-it runs as.";
42       type = types.str;
43     };
45     group = mkOption {
46       default = "snipeit";
47       description = "Group snipe-it runs as.";
48       type = types.str;
49     };
51     appKeyFile = mkOption {
52       description = ''
53         A file containing the Laravel APP_KEY - a 32 character long,
54         base64 encoded key used for encryption where needed. Can be
55         generated with `head -c 32 /dev/urandom | base64`.
56       '';
57       example = "/run/keys/snipe-it/appkey";
58       type = types.path;
59     };
61     hostName = lib.mkOption {
62       type = lib.types.str;
63       default = config.networking.fqdnOrHostName;
64       defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
65       example = "snipe-it.example.com";
66       description = ''
67         The hostname to serve Snipe-IT on.
68       '';
69     };
71     appURL = mkOption {
72       description = ''
73         The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
74         If you change this in the future you may need to run a command to update stored URLs in the database.
75         Command example: `snipe-it snipe-it:update-url https://old.example.com https://new.example.com`
76       '';
77       default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
78       defaultText = ''
79         http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
80       '';
81       example = "https://example.com";
82       type = types.str;
83     };
85     dataDir = mkOption {
86       description = "snipe-it data directory";
87       default = "/var/lib/snipe-it";
88       type = types.path;
89     };
91     database = {
92       host = mkOption {
93         type = types.str;
94         default = "localhost";
95         description = "Database host address.";
96       };
97       port = mkOption {
98         type = types.port;
99         default = 3306;
100         description = "Database host port.";
101       };
102       name = mkOption {
103         type = types.str;
104         default = "snipeit";
105         description = "Database name.";
106       };
107       user = mkOption {
108         type = types.str;
109         default = user;
110         defaultText = literalExpression "user";
111         description = "Database username.";
112       };
113       passwordFile = mkOption {
114         type = with types; nullOr path;
115         default = null;
116         example = "/run/keys/snipe-it/dbpassword";
117         description = ''
118           A file containing the password corresponding to
119           {option}`database.user`.
120         '';
121       };
122       createLocally = mkOption {
123         type = types.bool;
124         default = false;
125         description = "Create the database and database user locally.";
126       };
127     };
129     mail = {
130       driver = mkOption {
131         type = types.enum [ "smtp" "sendmail" ];
132         default = "smtp";
133         description = "Mail driver to use.";
134       };
135       host = mkOption {
136         type = types.str;
137         default = "localhost";
138         description = "Mail host address.";
139       };
140       port = mkOption {
141         type = types.port;
142         default = 1025;
143         description = "Mail host port.";
144       };
145       encryption = mkOption {
146         type = with types; nullOr (enum [ "tls" "ssl" ]);
147         default = null;
148         description = "SMTP encryption mechanism to use.";
149       };
150       user = mkOption {
151         type = with types; nullOr str;
152         default = null;
153         example = "snipeit";
154         description = "Mail username.";
155       };
156       passwordFile = mkOption {
157         type = with types; nullOr path;
158         default = null;
159         example = "/run/keys/snipe-it/mailpassword";
160         description = ''
161           A file containing the password corresponding to
162           {option}`mail.user`.
163         '';
164       };
165       backupNotificationAddress = mkOption {
166         type = types.str;
167         default = "backup@example.com";
168         description = "Email Address to send Backup Notifications to.";
169       };
170       from = {
171         name = mkOption {
172           type = types.str;
173           default = "Snipe-IT Asset Management";
174           description = "Mail \"from\" name.";
175         };
176         address = mkOption {
177           type = types.str;
178           default = "mail@example.com";
179           description = "Mail \"from\" address.";
180         };
181       };
182       replyTo = {
183         name = mkOption {
184           type = types.str;
185           default = "Snipe-IT Asset Management";
186           description = "Mail \"reply-to\" name.";
187         };
188         address = mkOption {
189           type = types.str;
190           default = "mail@example.com";
191           description = "Mail \"reply-to\" address.";
192         };
193       };
194     };
196     maxUploadSize = mkOption {
197       type = types.str;
198       default = "18M";
199       example = "1G";
200       description = "The maximum size for uploads (e.g. images).";
201     };
203     poolConfig = mkOption {
204       type = with types; attrsOf (oneOf [ str int bool ]);
205       default = {
206         "pm" = "dynamic";
207         "pm.max_children" = 32;
208         "pm.start_servers" = 2;
209         "pm.min_spare_servers" = 2;
210         "pm.max_spare_servers" = 4;
211         "pm.max_requests" = 500;
212       };
213       description = ''
214         Options for the snipe-it PHP pool. See the documentation on `php-fpm.conf`
215         for details on configuration directives.
216       '';
217     };
219     nginx = mkOption {
220       type = types.submodule (
221         recursiveUpdate
222           (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
223       );
224       default = {};
225       example = literalExpression ''
226         {
227           serverAliases = [
228             "snipe-it.''${config.networking.domain}"
229           ];
230           # To enable encryption and let let's encrypt take care of certificate
231           forceSSL = true;
232           enableACME = true;
233         }
234       '';
235       description = ''
236         With this option, you can customize the nginx virtualHost settings.
237       '';
238     };
240     config = mkOption {
241       type = with types;
242         attrsOf
243           (nullOr
244             (either
245               (oneOf [
246                 bool
247                 int
248                 port
249                 path
250                 str
251               ])
252               (submodule {
253                 options = {
254                   _secret = mkOption {
255                     type = nullOr (oneOf [ str path ]);
256                     description = ''
257                       The path to a file containing the value the
258                       option should be set to in the final
259                       configuration file.
260                     '';
261                   };
262                 };
263               })));
264       default = {};
265       example = literalExpression ''
266         {
267           ALLOWED_IFRAME_HOSTS = "https://example.com";
268           WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
269           AUTH_METHOD = "oidc";
270           OIDC_NAME = "MyLogin";
271           OIDC_DISPLAY_NAME_CLAIMS = "name";
272           OIDC_CLIENT_ID = "snipe-it";
273           OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
274           OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
275           OIDC_ISSUER_DISCOVER = true;
276         }
277       '';
278       description = ''
279         Snipe-IT configuration options to set in the
280         {file}`.env` file.
281         Refer to <https://snipe-it.readme.io/docs/configuration>
282         for details on supported values.
284         Settings containing secret data should be set to an attribute
285         set containing the attribute `_secret` - a
286         string pointing to a file containing the value the option
287         should be set to. See the example to get a better picture of
288         this: in the resulting {file}`.env` file, the
289         `OIDC_CLIENT_SECRET` key will be set to the
290         contents of the {file}`/run/keys/oidc_secret`
291         file.
292       '';
293     };
294   };
296   config = mkIf cfg.enable {
298     assertions = [
299       { assertion = db.createLocally -> db.user == user;
300         message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
301       }
302       { assertion = db.createLocally -> db.passwordFile == null;
303         message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
304       }
305     ];
307     environment.systemPackages = [ artisan ];
309     services.snipe-it.config = {
310       APP_ENV = "production";
311       APP_KEY._secret = cfg.appKeyFile;
312       APP_URL = cfg.appURL;
313       DB_HOST = db.host;
314       DB_PORT = db.port;
315       DB_DATABASE = db.name;
316       DB_USERNAME = db.user;
317       DB_PASSWORD._secret = db.passwordFile;
318       MAIL_DRIVER = mail.driver;
319       MAIL_FROM_NAME = mail.from.name;
320       MAIL_FROM_ADDR = mail.from.address;
321       MAIL_REPLYTO_NAME = mail.from.name;
322       MAIL_REPLYTO_ADDR = mail.from.address;
323       MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
324       MAIL_HOST = mail.host;
325       MAIL_PORT = mail.port;
326       MAIL_USERNAME = mail.user;
327       MAIL_ENCRYPTION = mail.encryption;
328       MAIL_PASSWORD._secret = mail.passwordFile;
329       APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
330       APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
331       APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
332       APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
333       APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
334       SESSION_SECURE_COOKIE = tlsEnabled;
335     };
337     services.mysql = mkIf db.createLocally {
338       enable = true;
339       package = mkDefault pkgs.mariadb;
340       ensureDatabases = [ db.name ];
341       ensureUsers = [
342         { name = db.user;
343           ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
344         }
345       ];
346     };
348     services.phpfpm.pools.snipe-it = {
349       inherit user group phpPackage;
350       phpOptions = ''
351         post_max_size = ${cfg.maxUploadSize}
352         upload_max_filesize = ${cfg.maxUploadSize}
353       '';
354       settings = {
355         "listen.mode" = "0660";
356         "listen.owner" = user;
357         "listen.group" = group;
358       } // cfg.poolConfig;
359     };
361     services.nginx = {
362       enable = mkDefault true;
363       virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
364         root = mkForce "${snipe-it}/share/php/snipe-it/public";
365         extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
366         locations = {
367           "/" = {
368             index = "index.php";
369             extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
370           };
371           "~ \.php$" = {
372             extraConfig = ''
373               try_files $uri $uri/ /index.php?$query_string;
374               include ${config.services.nginx.package}/conf/fastcgi_params;
375               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
376               fastcgi_param REDIRECT_STATUS 200;
377               fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
378               ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
379             '';
380           };
381           "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
382             extraConfig = "expires 365d;";
383           };
384         };
385       }];
386     };
388     systemd.services.snipe-it-setup = {
389       description = "Preparation tasks for snipe-it";
390       before = [ "phpfpm-snipe-it.service" ];
391       after = optional db.createLocally "mysql.service";
392       wantedBy = [ "multi-user.target" ];
393       serviceConfig = {
394         Type = "oneshot";
395         RemainAfterExit = true;
396         User = user;
397         WorkingDirectory = snipe-it;
398         RuntimeDirectory = "snipe-it/cache";
399         RuntimeDirectoryMode = "0700";
400       };
401       path = [ pkgs.replace-secret artisan ];
402       script =
403         let
404           isSecret  = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
405           snipeITEnvVars = lib.generators.toKeyValue {
406             mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
407               mkValueString = v: with builtins;
408                 if isInt             v then toString v
409                 else if isString     v then "\"${v}\""
410                 else if true  ==     v then "true"
411                 else if false ==     v then "false"
412                 else if isSecret     v then
413                   if (isString v._secret) then
414                     hashString "sha256" v._secret
415                   else
416                     hashString "sha256" (builtins.readFile v._secret)
417                 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
418             };
419           };
420           secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
421           mkSecretReplacement = file: ''
422             replace-secret ${escapeShellArgs [
423               (
424                 if (isString file) then
425                   builtins.hashString "sha256" file
426                 else
427                   builtins.hashString "sha256" (builtins.readFile file)
428               )
429               file
430               "${cfg.dataDir}/.env"
431             ]}
432           '';
433           secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
434           filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
435           snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
436         in ''
437           # error handling
438           set -euo pipefail
440           # set permissions
441           umask 077
443           # create .env file
444           install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
446           # replace secrets
447           ${secretReplacements}
449           # prepend `base64:` if it does not exist in APP_KEY
450           if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
451               sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
452           fi
454           # purge cache
455           rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
457           # migrate db
458           ${lib.getExe artisan} migrate --force
460           # A placeholder file for invalid barcodes
461           invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
462           if [ ! -e "$invalid_barcode_location" ]; then
463               cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
464           fi
465         '';
466     };
468     systemd.tmpfiles.rules = [
469       "d ${cfg.dataDir}                              0710 ${user} ${group} - -"
470       "d ${cfg.dataDir}/bootstrap                    0750 ${user} ${group} - -"
471       "d ${cfg.dataDir}/bootstrap/cache              0750 ${user} ${group} - -"
472       "d ${cfg.dataDir}/public                       0750 ${user} ${group} - -"
473       "d ${cfg.dataDir}/public/uploads               0750 ${user} ${group} - -"
474       "d ${cfg.dataDir}/public/uploads/accessories   0750 ${user} ${group} - -"
475       "d ${cfg.dataDir}/public/uploads/assets        0750 ${user} ${group} - -"
476       "d ${cfg.dataDir}/public/uploads/avatars       0750 ${user} ${group} - -"
477       "d ${cfg.dataDir}/public/uploads/barcodes      0750 ${user} ${group} - -"
478       "d ${cfg.dataDir}/public/uploads/categories    0750 ${user} ${group} - -"
479       "d ${cfg.dataDir}/public/uploads/companies     0750 ${user} ${group} - -"
480       "d ${cfg.dataDir}/public/uploads/components    0750 ${user} ${group} - -"
481       "d ${cfg.dataDir}/public/uploads/consumables   0750 ${user} ${group} - -"
482       "d ${cfg.dataDir}/public/uploads/departments   0750 ${user} ${group} - -"
483       "d ${cfg.dataDir}/public/uploads/locations     0750 ${user} ${group} - -"
484       "d ${cfg.dataDir}/public/uploads/manufacturers 0750 ${user} ${group} - -"
485       "d ${cfg.dataDir}/public/uploads/models        0750 ${user} ${group} - -"
486       "d ${cfg.dataDir}/public/uploads/suppliers     0750 ${user} ${group} - -"
487       "d ${cfg.dataDir}/storage                      0700 ${user} ${group} - -"
488       "d ${cfg.dataDir}/storage/app                  0700 ${user} ${group} - -"
489       "d ${cfg.dataDir}/storage/fonts                0700 ${user} ${group} - -"
490       "d ${cfg.dataDir}/storage/framework            0700 ${user} ${group} - -"
491       "d ${cfg.dataDir}/storage/framework/cache      0700 ${user} ${group} - -"
492       "d ${cfg.dataDir}/storage/framework/sessions   0700 ${user} ${group} - -"
493       "d ${cfg.dataDir}/storage/framework/views      0700 ${user} ${group} - -"
494       "d ${cfg.dataDir}/storage/logs                 0700 ${user} ${group} - -"
495       "d ${cfg.dataDir}/storage/uploads              0700 ${user} ${group} - -"
496       "d ${cfg.dataDir}/storage/private_uploads      0700 ${user} ${group} - -"
497     ];
499     users = {
500       users = mkIf (user == "snipeit") {
501         snipeit = {
502           inherit group;
503           isSystemUser = true;
504         };
505         "${config.services.nginx.user}".extraGroups = [ group ];
506       };
507       groups = mkIf (group == "snipeit") {
508         snipeit = {};
509       };
510     };
512   };
514   meta.maintainers = with maintainers; [ yayayayaka ];