0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-13 21:50:33 +03:00

Merge pull request #120440 from dotlambda/radicale-settings

nixos/radicale: add settings option
This commit is contained in:
Robert Schütz 2021-05-14 15:37:26 +02:00 committed by GitHub
commit e611d663f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 281 additions and 168 deletions

View file

@ -715,6 +715,20 @@ environment.systemPackages = [
The <package>yadm</package> dotfile manager has been updated from 2.x to 3.x, which has new (XDG) default locations for some data/state files. Most yadm commands will fail and print a legacy path warning (which describes how to upgrade/migrate your repository). If you have scripts, daemons, scheduled jobs, shell profiles, etc. that invoke yadm, expect them to fail or misbehave until you perform this migration and prepare accordingly. The <package>yadm</package> dotfile manager has been updated from 2.x to 3.x, which has new (XDG) default locations for some data/state files. Most yadm commands will fail and print a legacy path warning (which describes how to upgrade/migrate your repository). If you have scripts, daemons, scheduled jobs, shell profiles, etc. that invoke yadm, expect them to fail or misbehave until you perform this migration and prepare accordingly.
</para> </para>
</listitem> </listitem>
<listitem>
<para>
Instead of determining <option>services.radicale.package</option>
automatically based on <option>system.stateVersion</option>, the latest
version is always used because old versions are not officially supported.
</para>
<para>
Furthermore, Radicale's systemd unit was hardened which might break some
deployments. In particular, a non-default
<literal>filesystem_folder</literal> has to be added to
<option>systemd.services.radicale.serviceConfig.ReadWritePaths</option> if
the deprecated <option>services.radicale.config</option> is used.
</para>
</listitem>
</itemizedlist> </itemizedlist>
</section> </section>

View file

@ -3,56 +3,103 @@
with lib; with lib;
let let
cfg = config.services.radicale; cfg = config.services.radicale;
confFile = pkgs.writeText "radicale.conf" cfg.config; format = pkgs.formats.ini {
listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
defaultPackage = if versionAtLeast config.system.stateVersion "20.09" then {
pkg = pkgs.radicale3;
text = "pkgs.radicale3";
} else if versionAtLeast config.system.stateVersion "17.09" then {
pkg = pkgs.radicale2;
text = "pkgs.radicale2";
} else {
pkg = pkgs.radicale1;
text = "pkgs.radicale1";
};
in
{
options = {
services.radicale.enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable Radicale CalDAV and CardDAV server.
'';
}; };
services.radicale.package = mkOption { pkg = if isNull cfg.package then
type = types.package; pkgs.radicale
default = defaultPackage.pkg; else
defaultText = defaultPackage.text; cfg.package;
description = ''
Radicale package to use. This defaults to version 1.x if confFile = if cfg.settings == { } then
<literal>system.stateVersion &lt; 17.09</literal>, version 2.x if pkgs.writeText "radicale.conf" cfg.config
<literal>17.09 system.stateVersion &lt; 20.09</literal>, and else
version 3.x otherwise. format.generate "radicale.conf" cfg.settings;
'';
rightsFile = format.generate "radicale.rights" cfg.rights;
bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
in {
options.services.radicale = {
enable = mkEnableOption "Radicale CalDAV and CardDAV server";
package = mkOption {
description = "Radicale package to use.";
# Default cannot be pkgs.radicale because non-null values suppress
# warnings about incompatible configuration and storage formats.
type = with types; nullOr package // { inherit (package) description; };
default = null;
defaultText = "pkgs.radicale";
}; };
services.radicale.config = mkOption { config = mkOption {
type = types.str; type = types.str;
default = ""; default = "";
description = '' description = ''
Radicale configuration, this will set the service Radicale configuration, this will set the service
configuration file. configuration file.
This option is mutually exclusive with <option>settings</option>.
This option is deprecated. Use <option>settings</option> instead.
''; '';
}; };
services.radicale.extraArgs = mkOption { settings = mkOption {
type = format.type;
default = { };
description = ''
Configuration for Radicale. See
<link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
This option is mutually exclusive with <option>config</option>.
'';
example = literalExample ''
server = {
hosts = [ "0.0.0.0:5232" "[::]:5232" ];
};
auth = {
type = "htpasswd";
htpasswd_filename = "/etc/radicale/users";
htpasswd_encryption = "bcrypt";
};
storage = {
filesystem_folder = "/var/lib/radicale/collections";
};
'';
};
rights = mkOption {
type = format.type;
description = ''
Configuration for Radicale's rights file. See
<link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
This option only works in conjunction with <option>settings</option>.
Setting this will also set <option>settings.rights.type</option> and
<option>settings.rights.file</option> to approriate values.
'';
default = { };
example = literalExample ''
root = {
user = ".+";
collection = "";
permissions = "R";
};
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
'';
};
extraArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
description = "Extra arguments passed to the Radicale daemon."; description = "Extra arguments passed to the Radicale daemon.";
@ -60,33 +107,94 @@ in
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ]; assertions = [
{
assertion = cfg.settings == { } || cfg.config == "";
message = ''
The options services.radicale.config and services.radicale.settings
are mutually exclusive.
'';
}
];
users.users.radicale = warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
{ uid = config.ids.uids.radicale; The configuration and storage formats of your existing Radicale
description = "radicale user"; installation might be incompatible with the newest version.
home = "/var/lib/radicale"; For upgrade instructions see
createHome = true; https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
Set services.radicale.package to suppress this warning.
'' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
The configuration format of your existing Radicale installation might be
incompatible with the newest version. For upgrade instructions see
https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
Set services.radicale.package to suppress this warning.
'' ++ optional (cfg.config != "") ''
The option services.radicale.config is deprecated.
Use services.radicale.settings instead.
'';
services.radicale.settings.rights = mkIf (cfg.rights != { }) {
type = "from_file";
file = toString rightsFile;
}; };
users.groups.radicale = environment.systemPackages = [ pkg ];
{ gid = config.ids.gids.radicale; };
users.users.radicale.uid = config.ids.uids.radicale;
users.groups.radicale.gid = config.ids.gids.radicale;
systemd.services.radicale = { systemd.services.radicale = {
description = "A Simple Calendar and Contact Server"; description = "A Simple Calendar and Contact Server";
after = [ "network.target" ]; after = [ "network.target" ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
ExecStart = concatStringsSep " " ([ ExecStart = concatStringsSep " " ([
"${cfg.package}/bin/radicale" "-C" confFile "${pkg}/bin/radicale" "-C" confFile
] ++ ( ] ++ (
map escapeShellArg cfg.extraArgs map escapeShellArg cfg.extraArgs
)); ));
User = "radicale"; User = "radicale";
Group = "radicale"; Group = "radicale";
StateDirectory = "radicale/collections";
StateDirectoryMode = "0750";
# Hardening
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "/dev/stdin" ];
DevicePolicy = "strict";
IPAddressAllow = mkIf bindLocalhost "localhost";
IPAddressDeny = mkIf bindLocalhost "any";
LockPersonality = true;
MemoryDenyWriteExecute = 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";
ReadWritePaths = lib.optional
(hasAttrByPath [ "storage" "filesystem_folder" ] cfg.settings)
cfg.settings.storage.filesystem_folder;
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
UMask = "0027";
}; };
}; };
}; };
meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ]; meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
} }

View file

@ -1,140 +1,95 @@
import ./make-test-python.nix ({ lib, pkgs, ... }:
let let
user = "someuser"; user = "someuser";
password = "some_password"; password = "some_password";
port = builtins.toString 5232; port = "5232";
filesystem_folder = "/data/radicale";
common = { pkgs, ... }: { cli = "${pkgs.calendar-cli}/bin/calendar-cli --caldav-user ${user} --caldav-pass ${password}";
in {
name = "radicale3";
meta.maintainers = with lib.maintainers; [ dotlambda ];
machine = { pkgs, ... }: {
services.radicale = { services.radicale = {
enable = true; enable = true;
config = '' settings = {
[auth] auth = {
type = htpasswd type = "htpasswd";
htpasswd_filename = /etc/radicale/htpasswd htpasswd_filename = "/etc/radicale/users";
htpasswd_encryption = bcrypt htpasswd_encryption = "bcrypt";
[storage]
filesystem_folder = /tmp/collections
'';
}; };
storage = {
inherit filesystem_folder;
hook = "git add -A && (git diff --cached --quiet || git commit -m 'Changes by '%(user)s)";
};
logging.level = "info";
};
rights = {
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
};
};
systemd.services.radicale.path = [ pkgs.git ];
environment.systemPackages = [ pkgs.git ];
systemd.tmpfiles.rules = [ "d ${filesystem_folder} 0750 radicale radicale -" ];
# WARNING: DON'T DO THIS IN PRODUCTION! # WARNING: DON'T DO THIS IN PRODUCTION!
# This puts unhashed secrets directly into the Nix store for ease of testing. # This puts unhashed secrets directly into the Nix store for ease of testing.
environment.etc."radicale/htpasswd".source = pkgs.runCommand "htpasswd" {} '' environment.etc."radicale/users".source = pkgs.runCommand "htpasswd" {} ''
${pkgs.apacheHttpd}/bin/htpasswd -bcB "$out" ${user} ${password} ${pkgs.apacheHttpd}/bin/htpasswd -bcB "$out" ${user} ${password}
''; '';
}; };
testScript = ''
machine.wait_for_unit("radicale.service")
machine.wait_for_open_port(${port})
in machine.succeed("sudo -u radicale git -C ${filesystem_folder} init")
machine.succeed(
import ./make-test-python.nix ({ lib, ... }@args: { "sudo -u radicale git -C ${filesystem_folder} config --local user.email radicale@example.com"
name = "radicale";
meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ];
nodes = rec {
radicale = radicale1; # Make the test script read more nicely
radicale1 = lib.recursiveUpdate (common args) {
nixpkgs.overlays = [
(self: super: {
radicale1 = super.radicale1.overrideAttrs (oldAttrs: {
propagatedBuildInputs = with self.pythonPackages;
(oldAttrs.propagatedBuildInputs or []) ++ [ passlib ];
});
})
];
system.stateVersion = "17.03";
};
radicale1_export = lib.recursiveUpdate radicale1 {
services.radicale.extraArgs = [
"--export-storage" "/tmp/collections-new"
];
system.stateVersion = "17.03";
};
radicale2_verify = lib.recursiveUpdate radicale2 {
services.radicale.extraArgs = [ "--debug" "--verify-storage" ];
system.stateVersion = "17.09";
};
radicale2 = lib.recursiveUpdate (common args) {
system.stateVersion = "17.09";
};
radicale3 = lib.recursiveUpdate (common args) {
system.stateVersion = "20.09";
};
};
# This tests whether the web interface is accessible to an authenticated user
testScript = { nodes }: let
switchToConfig = nodeName: let
newSystem = nodes.${nodeName}.config.system.build.toplevel;
in "${newSystem}/bin/switch-to-configuration test";
in ''
with subtest("Check Radicale 1 functionality"):
radicale.succeed(
"${switchToConfig "radicale1"} >&2"
) )
radicale.wait_for_unit("radicale.service") machine.succeed(
radicale.wait_for_open_port(${port}) "sudo -u radicale git -C ${filesystem_folder} config --local user.name radicale"
radicale.succeed(
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
) )
with subtest("Export data in Radicale 2 format"): with subtest("Test calendar and event creation"):
radicale.succeed("systemctl stop radicale") machine.succeed(
radicale.succeed("ls -al /tmp/collections") "${cli} --caldav-url http://localhost:${port}/${user} calendar create cal"
radicale.fail("ls -al /tmp/collections-new")
with subtest("Radicale exits immediately after exporting storage"):
radicale.succeed(
"${switchToConfig "radicale1_export"} >&2"
) )
radicale.wait_until_fails("systemctl status radicale") machine.succeed("test -d ${filesystem_folder}/collection-root/${user}/cal")
radicale.succeed("ls -al /tmp/collections") machine.succeed('test -z "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
radicale.succeed("ls -al /tmp/collections-new") machine.succeed(
"${cli} --caldav-url http://localhost:${port}/${user}/cal calendar add 2021-04-23 testevent"
with subtest("Verify data in Radicale 2 format"):
radicale.succeed("rm -r /tmp/collections/${user}")
radicale.succeed("mv /tmp/collections-new/collection-root /tmp/collections")
radicale.succeed(
"${switchToConfig "radicale2_verify"} >&2"
) )
radicale.wait_until_fails("systemctl status radicale") machine.succeed('test -n "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
(status, stdout) = machine.execute(
(retcode, logs) = radicale.execute("journalctl -u radicale -n 10") "sudo -u radicale git -C ${filesystem_folder} log --format=oneline | wc -l"
assert (
retcode == 0 and "Verifying storage" in logs
), "Radicale 2 didn't verify storage"
assert (
"failed" not in logs and "exception" not in logs
), "storage verification failed"
with subtest("Check Radicale 2 functionality"):
radicale.succeed(
"${switchToConfig "radicale2"} >&2"
) )
radicale.wait_for_unit("radicale.service") assert status == 0, "git log failed"
radicale.wait_for_open_port(${port}) assert stdout == "3\n", "there should be exactly 3 commits"
(retcode, output) = radicale.execute( with subtest("Test rights file"):
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/" machine.fail(
"${cli} --caldav-url http://localhost:${port}/${user} calendar create sub/cal"
) )
assert ( machine.fail(
retcode == 0 and "VCALENDAR" in output "${cli} --caldav-url http://localhost:${port}/otheruser calendar create cal"
), "Could not read calendar from Radicale 2"
radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
with subtest("Check Radicale 3 functionality"):
radicale.succeed(
"${switchToConfig "radicale3"} >&2"
) )
radicale.wait_for_unit("radicale.service")
radicale.wait_for_open_port(${port})
(retcode, output) = radicale.execute( with subtest("Test web interface"):
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/" machine.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
)
assert (
retcode == 0 and "VCALENDAR" in output
), "Could not read calendar from Radicale 3"
radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/") with subtest("Test security"):
output = machine.succeed("systemd-analyze security radicale.service")
machine.log(output)
assert output[-9:-1] == "SAFE :-}"
''; '';
}) })

View file

@ -0,0 +1,34 @@
{ lib
, python3
, fetchFromGitHub
}:
python3.pkgs.buildPythonApplication rec {
pname = "calendar-cli";
version = "0.12.0";
src = fetchFromGitHub {
owner = "tobixen";
repo = "calendar-cli";
rev = "v${version}";
sha256 = "0qjld2m7hl3dx90491pqbjcja82c1f5gwx274kss4lkb8aw0kmlv";
};
propagatedBuildInputs = with python3.pkgs; [
icalendar
caldav
pytz
tzlocal
six
];
# tests require networking
doCheck = false;
meta = with lib; {
description = "Simple command-line CalDav client";
homepage = "https://github.com/tobixen/calendar-cli";
license = licenses.gpl3Plus;
maintainers = with maintainers; [ dotlambda ];
};
}

View file

@ -2012,6 +2012,8 @@ in
boost = pkgs.boost.override { python = python3; }; boost = pkgs.boost.override { python = python3; };
}; };
calendar-cli = callPackage ../tools/networking/calendar-cli { };
candle = libsForQt5.callPackage ../applications/misc/candle { }; candle = libsForQt5.callPackage ../applications/misc/candle { };
capstone = callPackage ../development/libraries/capstone { }; capstone = callPackage ../development/libraries/capstone { };