mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-26 08:53:21 +00:00
5b64b4072a
We mistakenly used a non-existing nginx variable for the X-Forwarded-Proto causing the well-known redirect to return erroneous Location headers like: Location: ://dav.example/dav instead of the correct: Location: https://dav.example/dav
555 lines
18 KiB
Nix
555 lines
18 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.services.davis;
|
|
db = cfg.database;
|
|
mail = cfg.mail;
|
|
|
|
mysqlLocal = db.createLocally && db.driver == "mysql";
|
|
pgsqlLocal = db.createLocally && db.driver == "postgresql";
|
|
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
|
|
isSecret = v: lib.isAttrs v && v ? _secret && (lib.isString v._secret || builtins.isPath v._secret);
|
|
davisEnvVars = lib.generators.toKeyValue {
|
|
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
|
mkValueString =
|
|
v:
|
|
if builtins.isInt v then
|
|
toString v
|
|
else if lib.isString v then
|
|
"\"${v}\""
|
|
else if true == v then
|
|
"true"
|
|
else if false == v then
|
|
"false"
|
|
else if null == v then
|
|
""
|
|
else if isSecret v then
|
|
if (lib.isString v._secret) then
|
|
builtins.hashString "sha256" v._secret
|
|
else
|
|
builtins.hashString "sha256" (builtins.readFile v._secret)
|
|
else
|
|
throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}";
|
|
};
|
|
};
|
|
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
|
|
mkSecretReplacement = file: ''
|
|
replace-secret ${
|
|
lib.escapeShellArgs [
|
|
(
|
|
if (lib.isString file) then
|
|
builtins.hashString "sha256" file
|
|
else
|
|
builtins.hashString "sha256" (builtins.readFile file)
|
|
)
|
|
file
|
|
"${cfg.dataDir}/.env.local"
|
|
]
|
|
}
|
|
'';
|
|
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
|
filteredConfig = lib.converge (lib.filterAttrsRecursive (
|
|
_: v:
|
|
!lib.elem v [
|
|
{ }
|
|
null
|
|
]
|
|
)) cfg.config;
|
|
davisEnv = pkgs.writeText "davis.env" (davisEnvVars filteredConfig);
|
|
in
|
|
{
|
|
options.services.davis = {
|
|
enable = lib.mkEnableOption "Davis is a caldav and carddav server";
|
|
|
|
user = lib.mkOption {
|
|
default = "davis";
|
|
description = "User davis runs as.";
|
|
type = lib.types.str;
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
default = "davis";
|
|
description = "Group davis runs as.";
|
|
type = lib.types.str;
|
|
};
|
|
|
|
package = lib.mkPackageOption pkgs "davis" { };
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/davis";
|
|
description = ''
|
|
Davis data directory.
|
|
'';
|
|
};
|
|
|
|
hostname = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "davis.yourdomain.org";
|
|
description = ''
|
|
Domain of the host to serve davis under. You may want to change it if you
|
|
run Davis on a different URL than davis.yourdomain.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.nullOr (
|
|
lib.types.either
|
|
(lib.types.oneOf [
|
|
lib.types.bool
|
|
lib.types.int
|
|
lib.types.port
|
|
lib.types.path
|
|
lib.types.str
|
|
])
|
|
(
|
|
lib.types.submodule {
|
|
options = {
|
|
_secret = lib.mkOption {
|
|
type = lib.types.nullOr (
|
|
lib.types.oneOf [
|
|
lib.types.str
|
|
lib.types.path
|
|
]
|
|
);
|
|
description = ''
|
|
The path to a file containing the value the
|
|
option should be set to in the final
|
|
configuration file.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
)
|
|
)
|
|
);
|
|
default = { };
|
|
|
|
example = '''';
|
|
description = '''';
|
|
};
|
|
|
|
adminLogin = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "root";
|
|
description = ''
|
|
Username for the admin account.
|
|
'';
|
|
};
|
|
adminPasswordFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = ''
|
|
The full path to a file that contains the admin's password. Must be
|
|
readable by the user.
|
|
'';
|
|
example = "/run/secrets/davis-admin-pass";
|
|
};
|
|
|
|
appSecretFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
description = ''
|
|
A file containing the Symfony APP_SECRET - Its value should be a series
|
|
of characters, numbers and symbols chosen randomly and the recommended
|
|
length is around 32 characters. Can be generated with <code>cat
|
|
/dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1</code>.
|
|
'';
|
|
example = "/run/secrets/davis-appsecret";
|
|
};
|
|
|
|
database = {
|
|
driver = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"sqlite"
|
|
"postgresql"
|
|
"mysql"
|
|
];
|
|
default = "sqlite";
|
|
description = "Database type, required in all circumstances.";
|
|
};
|
|
urlFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/run/secrets/davis-db-url";
|
|
description = ''
|
|
A file containing the database connection url. If set then it
|
|
overrides all other database settings (except driver). This is
|
|
mandatory if you want to use an external database, that is when
|
|
`services.davis.database.createLocally` is `false`.
|
|
'';
|
|
};
|
|
name = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = "davis";
|
|
description = "Database name, only used when the databse is created locally.";
|
|
};
|
|
createLocally = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Create the database and database user locally.";
|
|
};
|
|
};
|
|
|
|
mail = {
|
|
dsn = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Mail DSN for sending emails. Mutually exclusive with `services.davis.mail.dsnFile`.";
|
|
example = "smtp://username:password@example.com:25";
|
|
};
|
|
dsnFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "/run/secrets/davis-mail-dsn";
|
|
description = "A file containing the mail DSN for sending emails. Mutually exclusive with `servies.davis.mail.dsn`.";
|
|
};
|
|
inviteFromAddress = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Email address to send invitations from.";
|
|
example = "no-reply@dav.example.com";
|
|
};
|
|
};
|
|
|
|
nginx = lib.mkOption {
|
|
type = lib.types.submodule (
|
|
lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { }
|
|
);
|
|
default = null;
|
|
example = ''
|
|
{
|
|
serverAliases = [
|
|
"dav.''${config.networking.domain}"
|
|
];
|
|
# To enable encryption and let let's encrypt take care of certificate
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
}
|
|
'';
|
|
description = ''
|
|
With this option, you can customize the nginx virtualHost settings.
|
|
'';
|
|
};
|
|
|
|
poolConfig = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.oneOf [
|
|
lib.types.str
|
|
lib.types.int
|
|
lib.types.bool
|
|
]
|
|
);
|
|
default = {
|
|
"pm" = "dynamic";
|
|
"pm.max_children" = 32;
|
|
"pm.start_servers" = 2;
|
|
"pm.min_spare_servers" = 2;
|
|
"pm.max_spare_servers" = 4;
|
|
"pm.max_requests" = 500;
|
|
};
|
|
description = ''
|
|
Options for the davis PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
|
for details on configuration directives.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config =
|
|
let
|
|
defaultServiceConfig = {
|
|
ReadWritePaths = "${cfg.dataDir}";
|
|
User = user;
|
|
UMask = 77;
|
|
DeviceAllow = "";
|
|
LockPersonality = true;
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
PrivateTmp = true;
|
|
PrivateUsers = true;
|
|
ProcSubset = "pid";
|
|
ProtectClock = true;
|
|
ProtectControlGroups = true;
|
|
ProtectHome = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectProc = "invisible";
|
|
ProtectSystem = "strict";
|
|
RemoveIPC = true;
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
SystemCallArchitectures = "native";
|
|
SystemCallFilter = [
|
|
"@system-service"
|
|
"~@resources"
|
|
"~@privileged"
|
|
];
|
|
WorkingDirectory = "${cfg.package}/";
|
|
};
|
|
in
|
|
lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = db.createLocally -> db.urlFile == null;
|
|
message = "services.davis.database.urlFile must be unset if services.davis.database.createLocally is set true.";
|
|
}
|
|
{
|
|
assertion = db.createLocally || db.urlFile != null;
|
|
message = "One of services.davis.database.urlFile or services.davis.database.createLocally must be set.";
|
|
}
|
|
{
|
|
assertion = (mail.dsn != null) != (mail.dsnFile != null);
|
|
message = "One of (and only one of) services.davis.mail.dsn or services.davis.mail.dsnFile must be set.";
|
|
}
|
|
];
|
|
services.davis.config =
|
|
{
|
|
APP_ENV = "prod";
|
|
APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
|
|
# 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
|
|
# 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).
|
|
APP_LOG_DIR = "${cfg.dataDir}/var/log";
|
|
LOG_FILE_PATH = "/dev/stdout";
|
|
DATABASE_DRIVER = db.driver;
|
|
INVITE_FROM_ADDRESS = mail.inviteFromAddress;
|
|
APP_SECRET._secret = cfg.appSecretFile;
|
|
ADMIN_LOGIN = cfg.adminLogin;
|
|
ADMIN_PASSWORD._secret = cfg.adminPasswordFile;
|
|
APP_TIMEZONE = config.time.timeZone;
|
|
WEBDAV_ENABLED = false;
|
|
CALDAV_ENABLED = true;
|
|
CARDDAV_ENABLED = true;
|
|
}
|
|
// (if mail.dsn != null then { MAILER_DSN = mail.dsn; } else { MAILER_DSN._secret = mail.dsnFile; })
|
|
// (
|
|
if db.createLocally then
|
|
{
|
|
DATABASE_URL =
|
|
if db.driver == "sqlite" then
|
|
"sqlite:///${cfg.dataDir}/davis.db" # note: sqlite needs 4 slashes for an absolute path
|
|
else if
|
|
pgsqlLocal
|
|
# note: davis expects a non-standard postgres uri (due to the underlying doctrine library)
|
|
# specifically the dummy hostname which is overriden by the host query parameter
|
|
then
|
|
"postgres://${user}@localhost/${db.name}?host=/run/postgresql"
|
|
else if mysqlLocal then
|
|
"mysql://${user}@localhost/${db.name}?socket=/run/mysqld/mysqld.sock"
|
|
else
|
|
null;
|
|
}
|
|
else
|
|
{ DATABASE_URL._secret = db.urlFile; }
|
|
);
|
|
|
|
users = {
|
|
users = lib.mkIf (user == "davis") {
|
|
davis = {
|
|
description = "Davis service user";
|
|
group = cfg.group;
|
|
isSystemUser = true;
|
|
home = cfg.dataDir;
|
|
};
|
|
};
|
|
groups = lib.mkIf (group == "davis") { davis = { }; };
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var/log 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/var/cache 0700 ${user} ${group} - -"
|
|
];
|
|
|
|
services.phpfpm.pools.davis = {
|
|
inherit user group;
|
|
phpOptions = ''
|
|
log_errors = on
|
|
'';
|
|
phpEnv = {
|
|
ENV_DIR = "${cfg.dataDir}";
|
|
APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
|
|
APP_LOG_DIR = "${cfg.dataDir}/var/log";
|
|
};
|
|
settings =
|
|
{
|
|
"listen.mode" = "0660";
|
|
"pm" = "dynamic";
|
|
"pm.max_children" = 256;
|
|
"pm.start_servers" = 10;
|
|
"pm.min_spare_servers" = 5;
|
|
"pm.max_spare_servers" = 20;
|
|
}
|
|
// (
|
|
if cfg.nginx != null then
|
|
{
|
|
"listen.owner" = config.services.nginx.user;
|
|
"listen.group" = config.services.nginx.group;
|
|
}
|
|
else
|
|
{ }
|
|
)
|
|
// cfg.poolConfig;
|
|
};
|
|
|
|
# Reading the user-provided secret files requires root access
|
|
systemd.services.davis-env-setup = {
|
|
description = "Setup davis environment";
|
|
before = [
|
|
"phpfpm-davis.service"
|
|
"davis-db-migrate.service"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
path = [ pkgs.replace-secret ];
|
|
restartTriggers = [
|
|
cfg.package
|
|
davisEnv
|
|
];
|
|
script = ''
|
|
# error handling
|
|
set -euo pipefail
|
|
# create .env file with the upstream values
|
|
install -T -m 0600 -o ${user} ${cfg.package}/env-upstream "${cfg.dataDir}/.env"
|
|
# create .env.local file with the user-provided values
|
|
install -T -m 0600 -o ${user} ${davisEnv} "${cfg.dataDir}/.env.local"
|
|
${secretReplacements}
|
|
'';
|
|
};
|
|
|
|
systemd.services.davis-db-migrate = {
|
|
description = "Migrate davis database";
|
|
before = [ "phpfpm-davis.service" ];
|
|
after =
|
|
lib.optional mysqlLocal "mysql.service"
|
|
++ lib.optional pgsqlLocal "postgresql.service"
|
|
++ [ "davis-env-setup.service" ];
|
|
requires =
|
|
lib.optional mysqlLocal "mysql.service"
|
|
++ lib.optional pgsqlLocal "postgresql.service"
|
|
++ [ "davis-env-setup.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = defaultServiceConfig // {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
Environment = [
|
|
"ENV_DIR=${cfg.dataDir}"
|
|
"APP_CACHE_DIR=${cfg.dataDir}/var/cache"
|
|
"APP_LOG_DIR=${cfg.dataDir}/var/log"
|
|
];
|
|
EnvironmentFile = "${cfg.dataDir}/.env.local";
|
|
};
|
|
restartTriggers = [
|
|
cfg.package
|
|
davisEnv
|
|
];
|
|
script = ''
|
|
set -euo pipefail
|
|
${cfg.package}/bin/console cache:clear --no-debug
|
|
${cfg.package}/bin/console cache:warmup --no-debug
|
|
${cfg.package}/bin/console doctrine:migrations:migrate
|
|
'';
|
|
};
|
|
|
|
systemd.services.phpfpm-davis.after = [
|
|
"davis-env-setup.service"
|
|
"davis-db-migrate.service"
|
|
];
|
|
systemd.services.phpfpm-davis.requires = [
|
|
"davis-env-setup.service"
|
|
"davis-db-migrate.service"
|
|
] ++ lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.service";
|
|
systemd.services.phpfpm-davis.serviceConfig.ReadWritePaths = [ cfg.dataDir ];
|
|
|
|
services.nginx = lib.mkIf (cfg.nginx != null) {
|
|
enable = lib.mkDefault true;
|
|
virtualHosts = {
|
|
"${cfg.hostname}" = lib.mkMerge [
|
|
cfg.nginx
|
|
{
|
|
root = lib.mkForce "${cfg.package}/public";
|
|
extraConfig = ''
|
|
charset utf-8;
|
|
index index.php;
|
|
'';
|
|
locations = {
|
|
"/" = {
|
|
extraConfig = ''
|
|
try_files $uri $uri/ /index.php$is_args$args;
|
|
'';
|
|
};
|
|
"~* ^/.well-known/(caldav|carddav)$" = {
|
|
extraConfig = ''
|
|
return 302 https://$host/dav/;
|
|
'';
|
|
};
|
|
"~ ^(.+\.php)(.*)$" = {
|
|
extraConfig = ''
|
|
try_files $fastcgi_script_name =404;
|
|
include ${config.services.nginx.package}/conf/fastcgi_params;
|
|
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
|
fastcgi_pass unix:${config.services.phpfpm.pools.davis.socket};
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
fastcgi_split_path_info ^(.+\.php)(.*)$;
|
|
fastcgi_param X-Forwarded-Proto https;
|
|
fastcgi_param X-Forwarded-Port $http_x_forwarded_port;
|
|
'';
|
|
};
|
|
"~ /(\\.ht)" = {
|
|
extraConfig = ''
|
|
deny all;
|
|
return 404;
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
services.mysql = lib.mkIf mysqlLocal {
|
|
enable = true;
|
|
package = lib.mkDefault pkgs.mariadb;
|
|
ensureDatabases = [ db.name ];
|
|
ensureUsers = [
|
|
{
|
|
name = user;
|
|
ensurePermissions = {
|
|
"${db.name}.*" = "ALL PRIVILEGES";
|
|
};
|
|
}
|
|
];
|
|
};
|
|
|
|
services.postgresql = lib.mkIf pgsqlLocal {
|
|
enable = true;
|
|
ensureDatabases = [ db.name ];
|
|
ensureUsers = [
|
|
{
|
|
name = user;
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
meta = {
|
|
doc = ./davis.md;
|
|
maintainers = pkgs.davis.meta.maintainers;
|
|
};
|
|
}
|