nixos/rustical: init (#516007)

This commit is contained in:
Emily
2026-05-08 00:19:37 +00:00
committed by GitHub
6 changed files with 265 additions and 0 deletions
@@ -110,6 +110,8 @@
- [dsearch](https://github.com/AvengeMedia/danksearch), a fast filesystem search service with fuzzy matching. Available as [programs.dsearch](#opt-programs.dsearch.enable).
- [Rustical](https://github.com/lennart-k/rustical), a CalDav/CardDav server aiming to be simple, fast and passwordless. Available as [services.rustical](options.html#opt-services.rustical.enable).
- [Elephant](https://github.com/abenz1267/elephant), a data provider service and backend for building custom application launchers. Available as [services.elephant](#opt-services.elephant.enable).
- [Dunst](https://github.com/dunst-project/dunst), a lightweight and customizable notification daemon. Available as [services.dunst](#opt-services.dunst.enable).
+1
View File
@@ -1768,6 +1768,7 @@
./services/web-apps/rimgo.nix
./services/web-apps/rss-bridge.nix
./services/web-apps/rsshub.nix
./services/web-apps/rustical.nix
./services/web-apps/rutorrent.nix
./services/web-apps/screego.nix
./services/web-apps/selfoss.nix
@@ -0,0 +1,183 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkIf
mkEnableOption
mkOption
mkPackageOption
types
;
cfg = config.services.rustical;
format = pkgs.formats.toml { };
configFile = format.generate "rustical.toml" cfg.settings;
in
{
options.services.rustical = {
enable = mkEnableOption "RustiCali CalDAV/CardDAV server";
package = mkPackageOption pkgs "rustical" { };
settings = mkOption {
description = ''
Your {file}`/etc/rustical/config.toml` as a Nix attribute set.
Possible options can be found in the [Config struct]. A default
configuration can be viewed by running `rustical gen-config`.
[Config struct]: https://lennart-k.github.io/rustical/_crate/rustical/config/struct.Config.html
'';
type = types.submodule {
freeformType = format.type;
options = {
data_store.sqlite = {
db_url = mkOption {
type = types.path;
default = "/var/lib/rustical/db.sqlite3";
description = ''
Path where the sqlite database is stored.
'';
};
};
frontend = {
enabled = mkEnableOption "the HTTP frontend" // {
default = true;
};
};
http = {
host = mkOption {
type = types.str;
default = "[::1]";
example = "[::]";
description = ''
Host address to bind the HTTP service to.
:::{.note}
Rustical expects to be hosted behind a reverse proxy that
provides HTTPS. Without HTTPS, the web frontend and some clients
(e.g. Apple Calendar) may not work.
:::
'';
};
port = mkOption {
type = types.port;
default = 4000;
description = ''
Port to bind the HTTP service to.
'';
};
};
dav_push.enabled = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable [WebDav Push] support.
This allows the server to notify clients about changed data.
[WebDav Push]: https://github.com/bitfireAT/webdav-push/
'';
};
nextcloud_login.enabled = mkOption {
type = types.bool;
default = true;
description = ''
Whether to emulate the Nextcloud login flow.
This is supported in [DAVx5] and enables automatic app token generation.
[DAVx5]: https://www.davx5.com/
'';
};
};
};
};
environmentFiles = mkOption {
type = with types; listOf path;
default = [ ];
example = [ "/run/keys/rustical.env" ];
description = ''
Environment files to load into the runtime environment.
Check the documentation for how to construct [environment variables].
:::{.tip}
Environment variables can substitute any config value and are useful for
hiding secrets.
:::
[environment variables]: https://lennart-k.github.io/rustical/installation/configuration/#environment-variables
'';
};
};
config = mkIf cfg.enable {
# install the config at a path where the cli will find it
environment.etc."rustical/config.toml".source = configFile;
# provide the rustical cli
environment.systemPackages = [ cfg.package ];
systemd.services.rustical = {
description = "RustiCal CalDav/CardDav server";
documentation = [ "https://lennart-k.github.io/rustical/" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ configFile ];
serviceConfig = {
DynamicUser = true;
ExecStart = lib.getExe cfg.package;
EnvironmentFile = cfg.environmentFiles;
Restart = "on-failure";
StateDirectory = "rustical";
CapabilityBoundingSet = "";
DevicePolicy = "closed";
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";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @resources"
];
SystemCallErrorNumber = "EPERM";
UMask = "0077";
};
};
};
}
+1
View File
@@ -1469,6 +1469,7 @@ in
rtkit = runTest ./rtkit.nix;
rtorrent = runTest ./rtorrent.nix;
rush = runTest ./rush.nix;
rustical = runTest ./web-apps/rustical.nix;
rustls-libssl = runTest ./rustls-libssl.nix;
rxe = runTest ./rxe.nix;
sabnzbd = runTest ./sabnzbd.nix;
+72
View File
@@ -0,0 +1,72 @@
{
pkgs,
lib,
...
}:
{
name = "rustical";
meta.maintainers = pkgs.rustical.meta.maintainers;
nodes.machine =
{
pkgs,
...
}:
{
services.rustical.enable = true;
environment.systemPackages = with pkgs; [ calendar-cli ];
};
testScript =
{
nodes,
...
}:
let
port = toString nodes.machine.services.rustical.settings.http.port;
url = "http://localhost:${toString port}";
createPrincipalScript = pkgs.writeScript "rustical-create-principal" ''
#!${lib.getExe pkgs.expect}
spawn rustical principals create alice --password
expect "Enter your password:\r"
send "foobar\r"
expect eof
'';
calendarCliConfig = (pkgs.formats.json { }).generate "rustical-test-calendar-cli.json" {
default = {
caldav_user = "alice";
caldav_url = "${url}/caldav/";
calendar_url = "${url}/caldav/principal/alice";
};
testcal = {
inherits = "default";
calendar_url = "${url}/caldav/principal/alice/testcal";
};
};
in
# python
''
machine.wait_for_unit("rustical.service")
machine.wait_for_open_port(${port})
with subtest("Smoketest"):
machine.succeed("curl --fail ${url}")
with subtest("Create principal"):
machine.succeed("${createPrincipalScript}")
machine.succeed("rustical principals list | grep alice")
with subtest("Generate token for principal"):
machine.succeed("curl -f -c cookies.txt -d 'username=alice&password=foobar' ${url}/frontend/login")
machine.succeed("curl -f -b cookies.txt -d 'name=mytoken' ${url}/frontend/user/alice/app_token > token.txt")
with subtest("Interact with caldav"):
machine.succeed('calendar-cli --config-file ${calendarCliConfig} --caldav-pass "$(cat token.txt)" calendar create testcal')
machine.succeed('calendar-cli --config-file ${calendarCliConfig} --config-section testcal --caldav-pass "$(cat token.txt)" calendar add 2013-10-01 testevent')
machine.log(machine.execute("systemd-analyze security rustical.service | grep -v ")[1])
'';
}
+6
View File
@@ -4,6 +4,7 @@
fetchFromGitHub,
pkg-config,
openssl,
nixosTests,
}:
rustPlatform.buildRustPackage (finalAttrs: {
@@ -20,10 +21,15 @@ rustPlatform.buildRustPackage (finalAttrs: {
cargoHash = "sha256-uP1lZarcwQhBKyASQIiNUs053EuxJy112P2e3hy2uZY=";
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl ];
env.OPENSSL_NO_VENDOR = true;
passthru.tests = {
inherit (nixosTests) rustical;
};
meta = {
description = "Yet another calendar server aiming to be simple, fast and passwordless";
homepage = "https://github.com/lennart-k/rustical";