vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / misc / paperless.nix
blob73f09d804f29670b36163cba5f8aa1f4e33a2ce1
1 { config, pkgs, lib, ... }:
3 with lib;
4 let
5   cfg = config.services.paperless;
7   defaultUser = "paperless";
8   defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
10   # Don't start a redis instance if the user sets a custom redis connection
11   enableRedis = !(cfg.settings ? PAPERLESS_REDIS);
12   redisServer = config.services.redis.servers.paperless;
14   env = {
15     PAPERLESS_DATA_DIR = cfg.dataDir;
16     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
17     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
18     PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
19     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
20   } // optionalAttrs (config.time.timeZone != null) {
21     PAPERLESS_TIME_ZONE = config.time.timeZone;
22   } // optionalAttrs enableRedis {
23     PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
24   } // optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) {
25     PAPERLESS_NLTK_DIR = pkgs.symlinkJoin {
26       name = "paperless_ngx_nltk_data";
27       paths = cfg.package.nltkData;
28     };
29   } // optionalAttrs (cfg.openMPThreadingWorkaround) {
30     OMP_NUM_THREADS = "1";
31   } // (lib.mapAttrs (_: s:
32     if (lib.isAttrs s || lib.isList s) then builtins.toJSON s
33     else if lib.isBool s then lib.boolToString s
34     else toString s
35   ) cfg.settings);
37   manage = pkgs.writeShellScript "manage" ''
38     set -o allexport # Export the following env vars
39     ${lib.toShellVars env}
40     exec ${cfg.package}/bin/paperless-ngx "$@"
41   '';
43   defaultServiceConfig = {
44     Slice = "system-paperless.slice";
45     # Secure the services
46     ReadWritePaths = [
47       cfg.consumptionDir
48       cfg.dataDir
49       cfg.mediaDir
50     ];
51     CacheDirectory = "paperless";
52     CapabilityBoundingSet = "";
53     # ProtectClock adds DeviceAllow=char-rtc r
54     DeviceAllow = "";
55     LockPersonality = true;
56     MemoryDenyWriteExecute = true;
57     NoNewPrivileges = true;
58     PrivateDevices = true;
59     PrivateMounts = true;
60     PrivateNetwork = true;
61     PrivateTmp = true;
62     PrivateUsers = true;
63     ProtectClock = true;
64     # Breaks if the home dir of the user is in /home
65     # ProtectHome = true;
66     ProtectHostname = true;
67     ProtectSystem = "strict";
68     ProtectControlGroups = true;
69     ProtectKernelLogs = true;
70     ProtectKernelModules = true;
71     ProtectKernelTunables = true;
72     ProtectProc = "invisible";
73     # Don't restrict ProcSubset because django-q requires read access to /proc/stat
74     # to query CPU and memory information.
75     # Note that /proc only contains processes of user `paperless`, so this is safe.
76     # ProcSubset = "pid";
77     RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
78     RestrictNamespaces = true;
79     RestrictRealtime = true;
80     RestrictSUIDSGID = true;
81     SupplementaryGroups = optional enableRedis redisServer.user;
82     SystemCallArchitectures = "native";
83     SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
84     UMask = "0066";
85   };
88   meta.maintainers = with maintainers; [ leona SuperSandro2000 erikarvstedt ];
90   imports = [
91     (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
92     (mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ])
93   ];
95   options.services.paperless = {
96     enable = mkOption {
97       type = lib.types.bool;
98       default = false;
99       description = ''
100         Enable Paperless.
102         When started, the Paperless database is automatically created if it doesn't
103         exist and updated if the Paperless package has changed.
104         Both tasks are achieved by running a Django migration.
106         A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
107         `''${dataDir}/paperless-manage`.
108       '';
109     };
111     dataDir = mkOption {
112       type = types.str;
113       default = "/var/lib/paperless";
114       description = "Directory to store the Paperless data.";
115     };
117     mediaDir = mkOption {
118       type = types.str;
119       default = "${cfg.dataDir}/media";
120       defaultText = literalExpression ''"''${dataDir}/media"'';
121       description = "Directory to store the Paperless documents.";
122     };
124     consumptionDir = mkOption {
125       type = types.str;
126       default = "${cfg.dataDir}/consume";
127       defaultText = literalExpression ''"''${dataDir}/consume"'';
128       description = "Directory from which new documents are imported.";
129     };
131     consumptionDirIsPublic = mkOption {
132       type = types.bool;
133       default = false;
134       description = "Whether all users can write to the consumption dir.";
135     };
137     passwordFile = mkOption {
138       type = types.nullOr types.path;
139       default = null;
140       example = "/run/keys/paperless-password";
141       description = ''
142         A file containing the superuser password.
144         A superuser is required to access the web interface.
145         If unset, you can create a superuser manually by running
146         `''${dataDir}/paperless-manage createsuperuser`.
148         The default superuser name is `admin`. To change it, set
149         option {option}`settings.PAPERLESS_ADMIN_USER`.
150         WARNING: When changing the superuser name after the initial setup, the old superuser
151         will continue to exist.
153         To disable login for the web interface, set the following:
154         `settings.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`.
155         WARNING: Only use this on a trusted system without internet access to Paperless.
156       '';
157     };
159     address = mkOption {
160       type = types.str;
161       default = "localhost";
162       description = "Web interface address.";
163     };
165     port = mkOption {
166       type = types.port;
167       default = 28981;
168       description = "Web interface port.";
169     };
171     settings = mkOption {
172       type = lib.types.submodule {
173         freeformType = with lib.types; attrsOf (let
174           typeList = [ bool float int str path package ];
175         in oneOf (typeList ++ [ (listOf (oneOf typeList)) (attrsOf (oneOf typeList)) ]));
176       };
177       default = { };
178       description = ''
179         Extra paperless config options.
181         See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options.
183         Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values.
184         Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience.
185       '';
186       example = {
187         PAPERLESS_OCR_LANGUAGE = "deu+eng";
188         PAPERLESS_DBHOST = "/run/postgresql";
189         PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ];
190         PAPERLESS_OCR_USER_ARGS = {
191           optimize = 1;
192           pdfa_image_compression = "lossless";
193         };
194       };
195     };
197     user = mkOption {
198       type = types.str;
199       default = defaultUser;
200       description = "User under which Paperless runs.";
201     };
203     package = mkPackageOption pkgs "paperless-ngx" { } // {
204       apply = pkg: pkg.override {
205         tesseract5 = pkg.tesseract5.override {
206           # always enable detection modules
207           # tesseract fails to build when eng is not present
208           enableLanguages = if cfg.settings ? PAPERLESS_OCR_LANGUAGE then
209             lists.unique (
210               [ "equ" "osd" "eng" ]
211               ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE
212             )
213           else null;
214         };
215       };
216     };
218     openMPThreadingWorkaround = mkEnableOption ''
219       a workaround for document classifier timeouts.
221       Paperless uses OpenBLAS via scikit-learn for document classification.
223       The default is to use threading for OpenMP but this would cause the
224       document classifier to spin on one core seemingly indefinitely if there
225       are large amounts of classes per classification; causing it to
226       effectively never complete due to running into timeouts.
228       This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See
229       https://github.com/NixOS/nixpkgs/issues/240591 for more information
230     '' // mkOption { default = true; };
231   };
233   config = mkIf cfg.enable {
234     services.redis.servers.paperless.enable = mkIf enableRedis true;
236     systemd.slices.system-paperless = {
237       description = "Paperless Document Management System Slice";
238       documentation = [ "https://docs.paperless-ngx.com" ];
239     };
241     systemd.tmpfiles.settings."10-paperless" = let
242       defaultRule = {
243         inherit (cfg) user;
244         inherit (config.users.users.${cfg.user}) group;
245       };
246     in {
247       "${cfg.dataDir}".d = defaultRule;
248       "${cfg.mediaDir}".d = defaultRule;
249       "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
250     };
252     systemd.services.paperless-scheduler = {
253       description = "Paperless Celery Beat";
254       wantedBy = [ "multi-user.target" ];
255       wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
256       serviceConfig = defaultServiceConfig // {
257         User = cfg.user;
258         ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO";
259         Restart = "on-failure";
260         LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
261       };
262       environment = env;
264       preStart = ''
265         ln -sf ${manage} ${cfg.dataDir}/paperless-manage
267         # Auto-migrate on first run or if the package has changed
268         versionFile="${cfg.dataDir}/src-version"
269         version=$(cat "$versionFile" 2>/dev/null || echo 0)
271         if [[ $version != ${cfg.package.version} ]]; then
272           ${cfg.package}/bin/paperless-ngx migrate
274           # Parse old version string format for backwards compatibility
275           version=$(echo "$version" | grep -ohP '[^-]+$')
277           versionLessThan() {
278             target=$1
279             [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
280           }
282           if versionLessThan 1.12.0; then
283             # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
284             echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
285             ${cfg.package}/bin/paperless-ngx document_index reindex
286           fi
288           echo ${cfg.package.version} > "$versionFile"
289         fi
290       ''
291       + optionalString (cfg.passwordFile != null) ''
292         export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
293         export PAPERLESS_ADMIN_PASSWORD=$(cat $CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD)
294         superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
295         superuserStateFile="${cfg.dataDir}/superuser-state"
297         if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
298           ${cfg.package}/bin/paperless-ngx manage_superuser
299           echo "$superuserState" > "$superuserStateFile"
300         fi
301       '';
302     } // optionalAttrs enableRedis {
303       after = [ "redis-paperless.service" ];
304     };
306     systemd.services.paperless-task-queue = {
307       description = "Paperless Celery Workers";
308       after = [ "paperless-scheduler.service" ];
309       serviceConfig = defaultServiceConfig // {
310         User = cfg.user;
311         ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO";
312         Restart = "on-failure";
313         # The `mbind` syscall is needed for running the classifier.
314         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
315         # Needs to talk to mail server for automated import rules
316         PrivateNetwork = false;
317       };
318       environment = env;
319     };
321     systemd.services.paperless-consumer = {
322       description = "Paperless document consumer";
323       # Bind to `paperless-scheduler` so that the consumer never runs
324       # during migrations
325       bindsTo = [ "paperless-scheduler.service" ];
326       after = [ "paperless-scheduler.service" ];
327       serviceConfig = defaultServiceConfig // {
328         User = cfg.user;
329         ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
330         Restart = "on-failure";
331       };
332       environment = env;
333       # Allow the consumer to access the private /tmp directory of the server.
334       # This is required to support consuming files via a local folder.
335       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
336     };
338     systemd.services.paperless-web = {
339       description = "Paperless web server";
340       # Bind to `paperless-scheduler` so that the web server never runs
341       # during migrations
342       bindsTo = [ "paperless-scheduler.service" ];
343       after = [ "paperless-scheduler.service" ];
344       # Setup PAPERLESS_SECRET_KEY.
345       # If this environment variable is left unset, paperless-ngx defaults
346       # to a well-known value, which is insecure.
347       script = let
348         secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
349       in ''
350         if [[ ! -f '${secretKeyFile}' ]]; then
351           (
352             umask 0377
353             tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
354           )
355         fi
356         export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}')
357         if [[ ! $PAPERLESS_SECRET_KEY ]]; then
358           echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
359           exit 1
360         fi
361         exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \
362           -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
363       '';
364       serviceConfig = defaultServiceConfig // {
365         User = cfg.user;
366         Restart = "on-failure";
368         LimitNOFILE = 65536;
369         # gunicorn needs setuid, liblapack needs mbind
370         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
371         # Needs to serve web page
372         PrivateNetwork = false;
373       } // lib.optionalAttrs (cfg.port < 1024) {
374         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
375         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
376       };
377       environment = env // {
378         PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src";
379       };
380       # Allow the web interface to access the private /tmp directory of the server.
381       # This is required to support uploading files via the web interface.
382       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
383     };
385     users = optionalAttrs (cfg.user == defaultUser) {
386       users.${defaultUser} = {
387         group = defaultUser;
388         uid = config.ids.uids.paperless;
389         home = cfg.dataDir;
390       };
392       groups.${defaultUser} = {
393         gid = config.ids.gids.paperless;
394       };
395     };
396   };