···
9
+
cfg = config.services.davis;
13
+
mysqlLocal = db.createLocally && db.driver == "mysql";
14
+
pgsqlLocal = db.createLocally && db.driver == "postgresql";
19
+
isSecret = v: lib.isAttrs v && v ? _secret && (lib.isString v._secret || builtins.isPath v._secret);
20
+
davisEnvVars = lib.generators.toKeyValue {
21
+
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
24
+
if builtins.isInt v then
26
+
else if lib.isString v then
28
+
else if true == v then
30
+
else if false == v then
32
+
else if null == v then
34
+
else if isSecret v then
35
+
if (lib.isString v._secret) then
36
+
builtins.hashString "sha256" v._secret
38
+
builtins.hashString "sha256" (builtins.readFile v._secret)
40
+
throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}";
43
+
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
44
+
mkSecretReplacement = file: ''
46
+
lib.escapeShellArgs [
48
+
if (lib.isString file) then
49
+
builtins.hashString "sha256" file
51
+
builtins.hashString "sha256" (builtins.readFile file)
54
+
"${cfg.dataDir}/.env.local"
58
+
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
59
+
filteredConfig = lib.converge (lib.filterAttrsRecursive (
66
+
davisEnv = pkgs.writeText "davis.env" (davisEnvVars filteredConfig);
69
+
options.services.davis = {
70
+
enable = lib.mkEnableOption (lib.mdDoc "Davis is a caldav and carddav server");
72
+
user = lib.mkOption {
74
+
description = lib.mdDoc "User davis runs as.";
75
+
type = lib.types.str;
78
+
group = lib.mkOption {
80
+
description = lib.mdDoc "Group davis runs as.";
81
+
type = lib.types.str;
84
+
package = lib.mkPackageOption pkgs "davis" { };
86
+
dataDir = lib.mkOption {
87
+
type = lib.types.path;
88
+
default = "/var/lib/davis";
89
+
description = lib.mdDoc ''
90
+
Davis data directory.
94
+
hostname = lib.mkOption {
95
+
type = lib.types.str;
96
+
example = "davis.yourdomain.org";
97
+
description = lib.mdDoc ''
98
+
Domain of the host to serve davis under. You may want to change it if you
99
+
run Davis on a different URL than davis.yourdomain.
103
+
config = lib.mkOption {
104
+
type = lib.types.attrsOf (
115
+
lib.types.submodule {
117
+
_secret = lib.mkOption {
118
+
type = lib.types.nullOr (
124
+
description = lib.mdDoc ''
125
+
The path to a file containing the value the
126
+
option should be set to in the final
127
+
configuration file.
138
+
description = lib.mdDoc '''';
141
+
adminLogin = lib.mkOption {
142
+
type = lib.types.str;
144
+
description = lib.mdDoc ''
145
+
Username for the admin account.
148
+
adminPasswordFile = lib.mkOption {
149
+
type = lib.types.path;
150
+
description = lib.mdDoc ''
151
+
The full path to a file that contains the admin's password. Must be
152
+
readable by the user.
154
+
example = "/run/secrets/davis-admin-pass";
157
+
appSecretFile = lib.mkOption {
158
+
type = lib.types.path;
159
+
description = lib.mdDoc ''
160
+
A file containing the Symfony APP_SECRET - Its value should be a series
161
+
of characters, numbers and symbols chosen randomly and the recommended
162
+
length is around 32 characters. Can be generated with <code>cat
163
+
/dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1</code>.
165
+
example = "/run/secrets/davis-appsecret";
169
+
driver = lib.mkOption {
170
+
type = lib.types.enum [
175
+
default = "sqlite";
176
+
description = lib.mdDoc "Database type, required in all circumstances.";
178
+
urlFile = lib.mkOption {
179
+
type = lib.types.nullOr lib.types.path;
181
+
example = "/run/secrets/davis-db-url";
182
+
description = lib.mdDoc ''
183
+
A file containing the database connection url. If set then it
184
+
overrides all other database settings (except driver). This is
185
+
mandatory if you want to use an external database, that is when
186
+
`services.davis.database.createLocally` is `false`.
189
+
name = lib.mkOption {
190
+
type = lib.types.nullOr lib.types.str;
192
+
description = lib.mdDoc "Database name, only used when the databse is created locally.";
194
+
createLocally = lib.mkOption {
195
+
type = lib.types.bool;
197
+
description = lib.mdDoc "Create the database and database user locally.";
202
+
dsn = lib.mkOption {
203
+
type = lib.types.nullOr lib.types.str;
205
+
description = lib.mdDoc "Mail DSN for sending emails. Mutually exclusive with `services.davis.mail.dsnFile`.";
206
+
example = "smtp://username:password@example.com:25";
208
+
dsnFile = lib.mkOption {
209
+
type = lib.types.nullOr lib.types.str;
211
+
example = "/run/secrets/davis-mail-dsn";
212
+
description = lib.mdDoc "A file containing the mail DSN for sending emails. Mutually exclusive with `servies.davis.mail.dsn`.";
214
+
inviteFromAddress = lib.mkOption {
215
+
type = lib.types.nullOr lib.types.str;
217
+
description = lib.mdDoc "Email address to send invitations from.";
218
+
example = "no-reply@dav.example.com";
222
+
nginx = lib.mkOption {
223
+
type = lib.types.submodule (
224
+
lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { }
230
+
"dav.''${config.networking.domain}"
232
+
# To enable encryption and let let's encrypt take care of certificate
237
+
description = lib.mdDoc ''
238
+
With this option, you can customize the nginx virtualHost settings.
242
+
poolConfig = lib.mkOption {
243
+
type = lib.types.attrsOf (
252
+
"pm.max_children" = 32;
253
+
"pm.start_servers" = 2;
254
+
"pm.min_spare_servers" = 2;
255
+
"pm.max_spare_servers" = 4;
256
+
"pm.max_requests" = 500;
258
+
description = lib.mdDoc ''
259
+
Options for the davis PHP pool. See the documentation on <literal>php-fpm.conf</literal>
260
+
for details on configuration directives.
267
+
defaultServiceConfig = {
268
+
ReadWritePaths = "${cfg.dataDir}";
272
+
LockPersonality = true;
273
+
NoNewPrivileges = true;
274
+
PrivateDevices = true;
276
+
PrivateUsers = true;
277
+
ProcSubset = "pid";
278
+
ProtectClock = true;
279
+
ProtectControlGroups = true;
280
+
ProtectHome = true;
281
+
ProtectHostname = true;
282
+
ProtectKernelLogs = true;
283
+
ProtectKernelModules = true;
284
+
ProtectKernelTunables = true;
285
+
ProtectProc = "invisible";
286
+
ProtectSystem = "strict";
288
+
RestrictNamespaces = true;
289
+
RestrictRealtime = true;
290
+
RestrictSUIDSGID = true;
291
+
SystemCallArchitectures = "native";
292
+
SystemCallFilter = [
297
+
WorkingDirectory = "${cfg.package}/";
300
+
lib.mkIf cfg.enable {
303
+
assertion = db.createLocally -> db.urlFile == null;
304
+
message = "services.davis.database.urlFile must be unset if services.davis.database.createLocally is set true.";
307
+
assertion = db.createLocally || db.urlFile != null;
308
+
message = "One of services.davis.database.urlFile or services.davis.database.createLocally must be set.";
311
+
assertion = (mail.dsn != null) != (mail.dsnFile != null);
312
+
message = "One of (and only one of) services.davis.mail.dsn or services.davis.mail.dsnFile must be set.";
315
+
services.davis.config =
318
+
CACHE_DIR = "${cfg.dataDir}/var/cache";
319
+
# note: we do not need the log dir (we log to stdout/journald), by davis/symfony will try to create it, and the default value is one in the nix-store
320
+
# so we set it to a path under dataDir to avoid something like: Unable to create the "logs" directory (/nix/store/5cfskz0ybbx37s1161gjn5klwb5si1zg-davis-4.4.1/var/log).
321
+
LOG_DIR = "${cfg.dataDir}/var/log";
322
+
LOG_FILE_PATH = "/dev/stdout";
323
+
DATABASE_DRIVER = db.driver;
324
+
INVITE_FROM_ADDRESS = mail.inviteFromAddress;
325
+
APP_SECRET._secret = cfg.appSecretFile;
326
+
ADMIN_LOGIN = cfg.adminLogin;
327
+
ADMIN_PASSWORD._secret = cfg.adminPasswordFile;
328
+
APP_TIMEZONE = config.time.timeZone;
329
+
WEBDAV_ENABLED = false;
330
+
CALDAV_ENABLED = true;
331
+
CARDDAV_ENABLED = true;
333
+
// (if mail.dsn != null then { MAILER_DSN = mail.dsn; } else { MAILER_DSN._secret = mail.dsnFile; })
335
+
if db.createLocally then
338
+
if db.driver == "sqlite" then
339
+
"sqlite:///${cfg.dataDir}/davis.db" # note: sqlite needs 4 slashes for an absolute path
342
+
# note: davis expects a non-standard postgres uri (due to the underlying doctrine library)
343
+
# specifically the charset query parameter, and the dummy hostname which is overriden by the host query parameter
345
+
"postgres://${user}@localhost/${db.name}?host=/run/postgresql&charset=UTF-8"
346
+
else if mysqlLocal then
347
+
"mysql://${user}@localhost/${db.name}?socket=/run/mysqld/mysqld.sock"
352
+
{ DATABASE_URL._secret = db.urlFile; }
356
+
users = lib.mkIf (user == "davis") {
358
+
description = "Davis service user";
360
+
isSystemUser = true;
361
+
home = cfg.dataDir;
364
+
groups = lib.mkIf (group == "davis") { davis = { }; };
367
+
systemd.tmpfiles.rules = [
368
+
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
369
+
"d ${cfg.dataDir}/var 0700 ${user} ${group} - -"
370
+
"d ${cfg.dataDir}/var/log 0700 ${user} ${group} - -"
371
+
"d ${cfg.dataDir}/var/cache 0700 ${user} ${group} - -"
374
+
services.phpfpm.pools.davis = {
375
+
inherit user group;
380
+
ENV_DIR = "${cfg.dataDir}";
381
+
CACHE_DIR = "${cfg.dataDir}/var/cache";
382
+
#LOG_DIR = "${cfg.dataDir}/var/log";
386
+
"listen.mode" = "0660";
388
+
"pm.max_children" = 256;
389
+
"pm.start_servers" = 10;
390
+
"pm.min_spare_servers" = 5;
391
+
"pm.max_spare_servers" = 20;
394
+
if cfg.nginx != null then
396
+
"listen.owner" = config.services.nginx.user;
397
+
"listen.group" = config.services.nginx.group;
405
+
# Reading the user-provided secret files requires root access
406
+
systemd.services.davis-env-setup = {
407
+
description = "Setup davis environment";
409
+
"phpfpm-davis.service"
410
+
"davis-db-migrate.service"
412
+
wantedBy = [ "multi-user.target" ];
415
+
RemainAfterExit = true;
417
+
path = [ pkgs.replace-secret ];
418
+
restartTriggers = [
425
+
# create .env file with the upstream values
426
+
install -T -m 0600 -o ${user} ${cfg.package}/env-upstream "${cfg.dataDir}/.env"
427
+
# create .env.local file with the user-provided values
428
+
install -T -m 0600 -o ${user} ${davisEnv} "${cfg.dataDir}/.env.local"
429
+
${secretReplacements}
433
+
systemd.services.davis-db-migrate = {
434
+
description = "Migrate davis database";
435
+
before = [ "phpfpm-davis.service" ];
437
+
lib.optional mysqlLocal "mysql.service"
438
+
++ lib.optional pgsqlLocal "postgresql.service"
439
+
++ [ "davis-env-setup.service" ];
441
+
lib.optional mysqlLocal "mysql.service"
442
+
++ lib.optional pgsqlLocal "postgresql.service"
443
+
++ [ "davis-env-setup.service" ];
444
+
wantedBy = [ "multi-user.target" ];
445
+
serviceConfig = defaultServiceConfig // {
447
+
RemainAfterExit = true;
449
+
"ENV_DIR=${cfg.dataDir}"
450
+
"CACHE_DIR=${cfg.dataDir}/var/cache"
451
+
"LOG_DIR=${cfg.dataDir}/var/log"
453
+
EnvironmentFile = "${cfg.dataDir}/.env.local";
455
+
restartTriggers = [
461
+
${cfg.package}/bin/console cache:clear --no-debug
462
+
${cfg.package}/bin/console cache:warmup --no-debug
463
+
${cfg.package}/bin/console doctrine:migrations:migrate
467
+
systemd.services.phpfpm-davis.after = [
468
+
"davis-env-setup.service"
469
+
"davis-db-migrate.service"
471
+
systemd.services.phpfpm-davis.requires = [
472
+
"davis-env-setup.service"
473
+
"davis-db-migrate.service"
474
+
] ++ lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.service";
475
+
systemd.services.phpfpm-davis.serviceConfig.ReadWritePaths = [ cfg.dataDir ];
477
+
services.nginx = lib.mkIf (cfg.nginx != null) {
478
+
enable = lib.mkDefault true;
480
+
"${cfg.hostname}" = lib.mkMerge [
483
+
root = lib.mkForce "${cfg.package}/public";
491
+
try_files $uri $uri/ /index.php$is_args$args;
494
+
"~* ^/.well-known/(caldav|carddav)$" = {
496
+
return 302 $http_x_forwarded_proto://$host/dav/;
499
+
"~ ^(.+\.php)(.*)$" = {
501
+
try_files $fastcgi_script_name =404;
502
+
include ${config.services.nginx.package}/conf/fastcgi_params;
503
+
include ${config.services.nginx.package}/conf/fastcgi.conf;
504
+
fastcgi_pass unix:${config.services.phpfpm.pools.davis.socket};
505
+
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
506
+
fastcgi_param PATH_INFO $fastcgi_path_info;
507
+
fastcgi_split_path_info ^(.+\.php)(.*)$;
508
+
fastcgi_param X-Forwarded-Proto $http_x_forwarded_proto;
509
+
fastcgi_param X-Forwarded-Port $http_x_forwarded_port;
524
+
services.mysql = lib.mkIf mysqlLocal {
526
+
package = lib.mkDefault pkgs.mariadb;
527
+
ensureDatabases = [ db.name ];
531
+
ensurePermissions = {
532
+
"${db.name}.*" = "ALL PRIVILEGES";
538
+
services.postgresql = lib.mkIf pgsqlLocal {
540
+
ensureDatabases = [ db.name ];
544
+
ensureDBOwnership = true;
552
+
maintainers = pkgs.davis.meta.maintainers;