summary refs log tree commit diff
path: root/nixos/modules/services/networking/radicale.nix
blob: c6c40777ed7cf15793625aab7d0b43ffe1e6012a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.radicale;

  format = pkgs.formats.ini {
    listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
  };

  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;

  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 = literalExpression "pkgs.radicale";
    };

    config = mkOption {
      type = types.str;
      default = "";
      description = ''
        Radicale configuration, this will set the service
        configuration file.
        This option is mutually exclusive with <option>settings</option>.
        This option is deprecated.  Use <option>settings</option> instead.
      '';
    };

    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 = literalExpression ''
        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 = literalExpression ''
        root = {
          user = ".+";
          collection = "";
          permissions = "R";
        };
        principal = {
          user = ".+";
          collection = "{user}";
          permissions = "RW";
        };
        calendars = {
          user = ".+";
          collection = "{user}/[^/]+";
          permissions = "rw";
        };
      '';
    };

    extraArgs = mkOption {
      type = types.listOf types.str;
      default = [];
      description = "Extra arguments passed to the Radicale daemon.";
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.settings == { } || cfg.config == "";
        message = ''
          The options services.radicale.config and services.radicale.settings
          are mutually exclusive.
        '';
      }
    ];

    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 = {
      isSystemUser = true;
      group = "radicale";
    };

    users.groups.radicale = {};

    systemd.services.radicale = {
      description = "A Simple Calendar and Contact Server";
      after = [ "network.target" ];
      requires = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        ExecStart = concatStringsSep " " ([
          "${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";
        WorkingDirectory = "/var/lib/radicale";
      };
    };
  };

  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
}