diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml
index e3e6dc484330c..68dde82c18e31 100644
--- a/nixos/doc/manual/release-notes/rl-2105.xml
+++ b/nixos/doc/manual/release-notes/rl-2105.xml
@@ -715,6 +715,20 @@ environment.systemPackages = [
The yadm 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.
+
+
+ Instead of determining
+ automatically based on , the latest
+ version is always used because old versions are not officially supported.
+
+
+ Furthermore, Radicale's systemd unit was hardened which might break some
+ deployments. In particular, a non-default
+ filesystem_folder has to be added to
+ if
+ the deprecated is used.
+
+
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 5af035fd59e05..8c632c319d3c0 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -3,56 +3,103 @@
with lib;
let
-
cfg = config.services.radicale;
- confFile = pkgs.writeText "radicale.conf" cfg.config;
-
- 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";
+ format = pkgs.formats.ini {
+ listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
};
-in
-{
+ pkg = if isNull cfg.package then
+ pkgs.radicale
+ else
+ cfg.package;
+
+ confFile = if cfg.settings == { } then
+ pkgs.writeText "radicale.conf" cfg.config
+ else
+ format.generate "radicale.conf" cfg.settings;
+
+ rightsFile = format.generate "radicale.rights" cfg.rights;
- options = {
- services.radicale.enable = mkOption {
- type = types.bool;
- default = false;
+ 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";
+ };
+
+ config = mkOption {
+ type = types.str;
+ default = "";
description = ''
- Enable Radicale CalDAV and CardDAV server.
+ Radicale configuration, this will set the service
+ configuration file.
+ This option is mutually exclusive with .
+ This option is deprecated. Use instead.
'';
};
- services.radicale.package = mkOption {
- type = types.package;
- default = defaultPackage.pkg;
- defaultText = defaultPackage.text;
+ settings = mkOption {
+ type = format.type;
+ default = { };
description = ''
- Radicale package to use. This defaults to version 1.x if
- system.stateVersion < 17.09, version 2.x if
- 17.09 ≤ system.stateVersion < 20.09, and
- version 3.x otherwise.
+ Configuration for Radicale. See
+ .
+ This option is mutually exclusive with .
+ '';
+ 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";
+ };
'';
};
- services.radicale.config = mkOption {
- type = types.str;
- default = "";
+ rights = mkOption {
+ type = format.type;
description = ''
- Radicale configuration, this will set the service
- configuration file.
+ Configuration for Radicale's rights file. See
+ .
+ This option only works in conjunction with .
+ Setting this will also set and
+ to approriate values.
+ '';
+ default = { };
+ example = literalExample ''
+ root = {
+ user = ".+";
+ collection = "";
+ permissions = "R";
+ };
+ principal = {
+ user = ".+";
+ collection = "{user}";
+ permissions = "RW";
+ };
+ calendars = {
+ user = ".+";
+ collection = "{user}/[^/]+";
+ permissions = "rw";
+ };
'';
};
- services.radicale.extraArgs = mkOption {
+ extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Extra arguments passed to the Radicale daemon.";
@@ -60,33 +107,94 @@ in
};
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 =
- { uid = config.ids.uids.radicale;
- description = "radicale user";
- home = "/var/lib/radicale";
- createHome = true;
- };
+ warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
+ The configuration and storage formats of your existing Radicale
+ installation might be incompatible with the newest version.
+ For upgrade instructions see
+ 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;
+ };
+
+ environment.systemPackages = [ pkg ];
+
+ users.users.radicale.uid = config.ids.uids.radicale;
- users.groups.radicale =
- { gid = config.ids.gids.radicale; };
+ users.groups.radicale.gid = config.ids.gids.radicale;
systemd.services.radicale = {
description = "A Simple Calendar and Contact Server";
after = [ "network.target" ];
+ requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = concatStringsSep " " ([
- "${cfg.package}/bin/radicale" "-C" confFile
+ "${pkg}/bin/radicale" "-C" confFile
] ++ (
map escapeShellArg cfg.extraArgs
));
User = "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 ];
}
diff --git a/nixos/tests/radicale.nix b/nixos/tests/radicale.nix
index 1d3679c82a20c..5101628a682c6 100644
--- a/nixos/tests/radicale.nix
+++ b/nixos/tests/radicale.nix
@@ -1,140 +1,95 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
let
user = "someuser";
password = "some_password";
- port = builtins.toString 5232;
+ port = "5232";
+ filesystem_folder = "/data/radicale";
+
+ cli = "${pkgs.calendar-cli}/bin/calendar-cli --caldav-user ${user} --caldav-pass ${password}";
+in {
+ name = "radicale3";
+ meta.maintainers = with lib.maintainers; [ dotlambda ];
- common = { pkgs, ... }: {
+ machine = { pkgs, ... }: {
services.radicale = {
enable = true;
- config = ''
- [auth]
- type = htpasswd
- htpasswd_filename = /etc/radicale/htpasswd
- htpasswd_encryption = bcrypt
-
- [storage]
- filesystem_folder = /tmp/collections
- '';
+ settings = {
+ auth = {
+ type = "htpasswd";
+ htpasswd_filename = "/etc/radicale/users";
+ htpasswd_encryption = "bcrypt";
+ };
+ 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!
# 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}
'';
};
-
-in
-
- import ./make-test-python.nix ({ lib, ... }@args: {
- 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")
- radicale.wait_for_open_port(${port})
- radicale.succeed(
- "curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
- )
-
- with subtest("Export data in Radicale 2 format"):
- radicale.succeed("systemctl stop radicale")
- radicale.succeed("ls -al /tmp/collections")
- 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")
- radicale.succeed("ls -al /tmp/collections")
- radicale.succeed("ls -al /tmp/collections-new")
-
- 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")
-
- (retcode, logs) = radicale.execute("journalctl -u radicale -n 10")
- 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")
- radicale.wait_for_open_port(${port})
-
- (retcode, output) = radicale.execute(
- "curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
- )
- assert (
- retcode == 0 and "VCALENDAR" in output
- ), "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(
- "curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
- )
- assert (
- retcode == 0 and "VCALENDAR" in output
- ), "Could not read calendar from Radicale 3"
-
- radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
- '';
+ testScript = ''
+ machine.wait_for_unit("radicale.service")
+ machine.wait_for_open_port(${port})
+
+ machine.succeed("sudo -u radicale git -C ${filesystem_folder} init")
+ machine.succeed(
+ "sudo -u radicale git -C ${filesystem_folder} config --local user.email radicale@example.com"
+ )
+ machine.succeed(
+ "sudo -u radicale git -C ${filesystem_folder} config --local user.name radicale"
+ )
+
+ with subtest("Test calendar and event creation"):
+ machine.succeed(
+ "${cli} --caldav-url http://localhost:${port}/${user} calendar create cal"
+ )
+ machine.succeed("test -d ${filesystem_folder}/collection-root/${user}/cal")
+ machine.succeed('test -z "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
+ machine.succeed(
+ "${cli} --caldav-url http://localhost:${port}/${user}/cal calendar add 2021-04-23 testevent"
+ )
+ machine.succeed('test -n "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
+ (status, stdout) = machine.execute(
+ "sudo -u radicale git -C ${filesystem_folder} log --format=oneline | wc -l"
+ )
+ assert status == 0, "git log failed"
+ assert stdout == "3\n", "there should be exactly 3 commits"
+
+ with subtest("Test rights file"):
+ machine.fail(
+ "${cli} --caldav-url http://localhost:${port}/${user} calendar create sub/cal"
+ )
+ machine.fail(
+ "${cli} --caldav-url http://localhost:${port}/otheruser calendar create cal"
+ )
+
+ with subtest("Test web interface"):
+ machine.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 :-}"
+ '';
})
diff --git a/pkgs/tools/networking/calendar-cli/default.nix b/pkgs/tools/networking/calendar-cli/default.nix
new file mode 100644
index 0000000000000..497b77b57ac0a
--- /dev/null
+++ b/pkgs/tools/networking/calendar-cli/default.nix
@@ -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 ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 04effbffbd3d4..fb14746f482c3 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -1996,6 +1996,8 @@ in
boost = pkgs.boost.override { python = python3; };
};
+ calendar-cli = callPackage ../tools/networking/calendar-cli { };
+
candle = libsForQt5.callPackage ../applications/misc/candle { };
capstone = callPackage ../development/libraries/capstone { };