python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / paperless.nix
blob6a98d5cb686d3416a297db2c3caa898a72967d9b
1 { config, pkgs, lib, ... }:
3 with lib;
4 let
5   cfg = config.services.paperless;
6   pkg = cfg.package;
8   defaultUser = "paperless";
10   # Don't start a redis instance if the user sets a custom redis connection
11   enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
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     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
19   } // optionalAttrs (config.time.timeZone != null) {
20     PAPERLESS_TIME_ZONE = config.time.timeZone;
21   } // optionalAttrs enableRedis {
22     PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
23   } // (
24     lib.mapAttrs (_: toString) cfg.extraConfig
25   );
27   manage = let
28     setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
29   in pkgs.writeShellScript "manage" ''
30     ${setupEnv}
31     exec ${pkg}/bin/paperless-ngx "$@"
32   '';
34   # Secure the services
35   defaultServiceConfig = {
36     TemporaryFileSystem = "/:ro";
37     BindReadOnlyPaths = [
38       "/nix/store"
39       "-/etc/resolv.conf"
40       "-/etc/nsswitch.conf"
41       "-/etc/hosts"
42       "-/etc/localtime"
43       "-/run/postgresql"
44     ] ++ (optional enableRedis redisServer.unixSocket);
45     BindPaths = [
46       cfg.consumptionDir
47       cfg.dataDir
48       cfg.mediaDir
49     ];
50     CapabilityBoundingSet = "";
51     # ProtectClock adds DeviceAllow=char-rtc r
52     DeviceAllow = "";
53     LockPersonality = true;
54     MemoryDenyWriteExecute = true;
55     NoNewPrivileges = true;
56     PrivateDevices = true;
57     PrivateMounts = true;
58     PrivateNetwork = true;
59     PrivateTmp = true;
60     PrivateUsers = true;
61     ProtectClock = true;
62     # Breaks if the home dir of the user is in /home
63     # Also does not add much value in combination with the TemporaryFileSystem.
64     # ProtectHome = true;
65     ProtectHostname = true;
66     # Would re-mount paths ignored by temporary root
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     # Does not work well with the temporary root
85     #UMask = "0066";
86   };
89   meta.maintainers = with maintainers; [ erikarvstedt Flakebi ];
91   imports = [
92     (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
93   ];
95   options.services.paperless = {
96     enable = mkOption {
97       type = lib.types.bool;
98       default = false;
99       description = lib.mdDoc ''
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 = lib.mdDoc "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 = lib.mdDoc "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 = lib.mdDoc "Directory from which new documents are imported.";
129     };
131     consumptionDirIsPublic = mkOption {
132       type = types.bool;
133       default = false;
134       description = lib.mdDoc "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 = lib.mdDoc ''
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}`extraConfig.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         `extraConfig.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 = lib.mdDoc "Web interface address.";
163     };
165     port = mkOption {
166       type = types.port;
167       default = 28981;
168       description = lib.mdDoc "Web interface port.";
169     };
171     extraConfig = mkOption {
172       type = types.attrs;
173       default = {};
174       description = lib.mdDoc ''
175         Extra paperless config options.
177         See [the documentation](https://paperless-ngx.readthedocs.io/en/latest/configuration.html)
178         for available options.
179       '';
180       example = {
181         PAPERLESS_OCR_LANGUAGE = "deu+eng";
182         PAPERLESS_DBHOST = "/run/postgresql";
183       };
184     };
186     user = mkOption {
187       type = types.str;
188       default = defaultUser;
189       description = lib.mdDoc "User under which Paperless runs.";
190     };
192     package = mkOption {
193       type = types.package;
194       default = pkgs.paperless-ngx;
195       defaultText = literalExpression "pkgs.paperless-ngx";
196       description = lib.mdDoc "The Paperless package to use.";
197     };
198   };
200   config = mkIf cfg.enable {
201     services.redis.servers.paperless.enable = mkIf enableRedis true;
203     systemd.tmpfiles.rules = [
204       "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
205       "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
206       (if cfg.consumptionDirIsPublic then
207         "d '${cfg.consumptionDir}' 777 - - - -"
208       else
209         "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
210       )
211     ];
213     systemd.services.paperless-scheduler = {
214       description = "Paperless scheduler";
215       serviceConfig = defaultServiceConfig // {
216         User = cfg.user;
217         ExecStart = "${pkg}/bin/paperless-ngx qcluster";
218         Restart = "on-failure";
219         # The `mbind` syscall is needed for running the classifier.
220         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
221         # Needs to talk to mail server for automated import rules
222         PrivateNetwork = false;
223       };
224       environment = env;
225       wantedBy = [ "multi-user.target" ];
226       wants = [ "paperless-consumer.service" "paperless-web.service" ];
228       preStart = ''
229         ln -sf ${manage} ${cfg.dataDir}/paperless-manage
231         # Auto-migrate on first run or if the package has changed
232         versionFile="${cfg.dataDir}/src-version"
233         if [[ $(cat "$versionFile" 2>/dev/null) != ${pkg} ]]; then
234           ${pkg}/bin/paperless-ngx migrate
235           echo ${pkg} > "$versionFile"
236         fi
237       ''
238       + optionalString (cfg.passwordFile != null) ''
239         export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
240         export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
241         superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
242         superuserStateFile="${cfg.dataDir}/superuser-state"
244         if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
245           ${pkg}/bin/paperless-ngx manage_superuser
246           echo "$superuserState" > "$superuserStateFile"
247         fi
248       '';
249     } // optionalAttrs enableRedis {
250       after = [ "redis-paperless.service" ];
251     };
253     # Reading the user-provided password file requires root access
254     systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
255       requiredBy = [ "paperless-scheduler.service" ];
256       before = [ "paperless-scheduler.service" ];
257       serviceConfig = {
258         ExecStart = ''
259           ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
260             '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
261         '';
262         Type = "oneshot";
263       };
264     };
266     systemd.services.paperless-consumer = {
267       description = "Paperless document consumer";
268       serviceConfig = defaultServiceConfig // {
269         User = cfg.user;
270         ExecStart = "${pkg}/bin/paperless-ngx document_consumer";
271         Restart = "on-failure";
272       };
273       environment = env;
274       # Bind to `paperless-scheduler` so that the consumer never runs
275       # during migrations
276       bindsTo = [ "paperless-scheduler.service" ];
277       after = [ "paperless-scheduler.service" ];
278     };
280     systemd.services.paperless-web = {
281       description = "Paperless web server";
282       serviceConfig = defaultServiceConfig // {
283         User = cfg.user;
284         ExecStart = ''
285           ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
286             -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
287         '';
288         Restart = "on-failure";
290         # gunicorn needs setuid, liblapack needs mbind
291         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
292         # Needs to serve web page
293         PrivateNetwork = false;
294       } // lib.optionalAttrs (cfg.port < 1024) {
295         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
296         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
297       };
298       environment = env // {
299         PATH = mkForce pkg.path;
300         PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src";
301       };
302       # Allow the web interface to access the private /tmp directory of the server.
303       # This is required to support uploading files via the web interface.
304       unitConfig.JoinsNamespaceOf = "paperless-scheduler.service";
305       # Bind to `paperless-scheduler` so that the web server never runs
306       # during migrations
307       bindsTo = [ "paperless-scheduler.service" ];
308       after = [ "paperless-scheduler.service" ];
309     };
311     users = optionalAttrs (cfg.user == defaultUser) {
312       users.${defaultUser} = {
313         group = defaultUser;
314         uid = config.ids.uids.paperless;
315         home = cfg.dataDir;
316       };
318       groups.${defaultUser} = {
319         gid = config.ids.gids.paperless;
320       };
321     };
322   };