diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index a014c93afedec..bc5f872dd3b6e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1482,6 +1482,7 @@ ./services/web-apps/ocis.nix ./services/web-apps/onlyoffice.nix ./services/web-apps/openvscode-server.nix + ./services/web-apps/open-web-calendar.nix ./services/web-apps/mobilizon.nix ./services/web-apps/openwebrx.nix ./services/web-apps/outline.nix diff --git a/nixos/modules/services/web-apps/open-web-calendar.nix b/nixos/modules/services/web-apps/open-web-calendar.nix new file mode 100644 index 0000000000000..525c9eb8c28cd --- /dev/null +++ b/nixos/modules/services/web-apps/open-web-calendar.nix @@ -0,0 +1,162 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkIf + mkOption + mkEnableOption + mkPackageOption + mkDefault + types + concatMapStringsSep + generators + ; + cfg = config.services.open-web-calendar; + + nixosSpec = calendarSettingsFormat.generate "nixos_specification.json" cfg.calendarSettings; + finalPackage = cfg.package.override { + # The calendarSettings need to be merged with the default_specification.yml + # in the source. This way we use upstreams default values but keep everything overridable. + defaultSpecificationFile = pkgs.runCommand "custom-default_specification.yml" { } '' + ${pkgs.yq}/bin/yq -s '.[0] * .[1]' ${cfg.package}/${cfg.package.defaultSpecificationPath} ${nixosSpec} > $out + ''; + }; + + inherit (finalPackage) python; + pythonEnv = python.buildEnv.override { + extraLibs = [ + (python.pkgs.toPythonModule finalPackage) + # Allows Gunicorn to set a meaningful process name + python.pkgs.gunicorn.optional-dependencies.setproctitle + ]; + }; + + settingsFormat = pkgs.formats.keyValue { }; + calendarSettingsFormat = pkgs.formats.json { }; +in +{ + options.services.open-web-calendar = { + + enable = mkEnableOption "OpenWebCalendar service"; + + package = mkPackageOption pkgs "open-web-calendar" { }; + + domain = mkOption { + type = types.str; + description = "The domain under which open-web-calendar is made available"; + example = "open-web-calendar.example.org"; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + ALLOWED_HOSTS = mkOption { + type = types.str; + readOnly = true; + description = '' + The hosts that the Open Web Calendar permits. This is required to + mitigate the Host Header Injection vulnerability. + + We always set this to the empty list, as Nginx already checks the Host header. + ''; + default = ""; + }; + }; + }; + default = { }; + description = '' + Configuration for the server. These are set as environment variables to the gunicorn/flask service. + + See the documentation options in . + ''; + }; + + calendarSettings = mkOption { + type = types.submodule { + freeformType = calendarSettingsFormat.type; + options = { }; + }; + default = { }; + description = '' + Configure the default calendar. + + See the documentation options in and . + + Individual calendar instances can be further configured outside this module, by specifying the `specification_url` parameter. + ''; + }; + + }; + + config = mkIf cfg.enable { + + assertions = [ + { + assertion = !cfg.settings ? "PORT"; + message = '' + services.open-web-calendar.settings.PORT can't be set, as the service uses a unix socket. + ''; + } + ]; + + systemd.sockets.open-web-calendar = { + before = [ "nginx.service" ]; + wantedBy = [ "sockets.target" ]; + socketConfig = { + ListenStream = "/run/open-web-calendar/socket"; + SocketUser = "open-web-calendar"; + SocketGroup = "open-web-calendar"; + SocketMode = "770"; + }; + }; + + systemd.services.open-web-calendar = { + description = "Open Web Calendar"; + after = [ "network.target" ]; + environment.PYTHONPATH = "${pythonEnv}/${python.sitePackages}/"; + serviceConfig = { + Type = "notify"; + NotifyAccess = "all"; + ExecStart = '' + ${pythonEnv.pkgs.gunicorn}/bin/gunicorn \ + --name=open-web-calendar \ + --bind='unix:///run/open-web-calendar/socket' \ + open_web_calendar.app:app + ''; + EnvironmentFile = settingsFormat.generate "open-web-calendar.env" cfg.settings; + ExecReload = "kill -s HUP $MAINPID"; + KillMode = "mixed"; + PrivateTmp = true; + RuntimeDirectory = "open-web-calendar"; + User = "open-web-calendar"; + Group = "open-web-calendar"; + }; + }; + + users.users.open-web-calendar = { + isSystemUser = true; + group = "open-web-calendar"; + }; + + services.nginx = { + enable = true; + virtualHosts."${cfg.domain}" = { + forceSSL = mkDefault true; + enableACME = mkDefault true; + locations."/".proxyPass = "http://unix:///run/open-web-calendar/socket"; + }; + }; + + users.groups.open-web-calendar.members = [ config.services.nginx.user ]; + + }; + + meta.maintainers = with lib.maintainers; [ erictapen ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3fcc429f3478f..f5a5fa37772f7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -745,6 +745,7 @@ in { openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {}; opentabletdriver = handleTest ./opentabletdriver.nix {}; opentelemetry-collector = handleTest ./opentelemetry-collector.nix {}; + open-web-calendar = handleTest ./web-apps/open-web-calendar.nix {}; ocsinventory-agent = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./ocsinventory-agent.nix {}; owncast = handleTest ./owncast.nix {}; outline = handleTest ./outline.nix {}; diff --git a/nixos/tests/web-apps/open-web-calendar.nix b/nixos/tests/web-apps/open-web-calendar.nix new file mode 100644 index 0000000000000..b5cd59c56abdf --- /dev/null +++ b/nixos/tests/web-apps/open-web-calendar.nix @@ -0,0 +1,51 @@ +import ../make-test-python.nix ( + { pkgs, ... }: + + let + certs = import ../common/acme/server/snakeoil-certs.nix; + + serverDomain = certs.domain; + in + { + name = "open-web-calendar"; + meta.maintainers = with pkgs.lib.maintainers; [ erictapen ]; + + nodes.server = + { pkgs, lib, ... }: + { + services.open-web-calendar = { + enable = true; + domain = serverDomain; + calendarSettings.title = "My custom title"; + }; + + services.nginx.virtualHosts."${serverDomain}" = { + enableACME = lib.mkForce false; + sslCertificate = certs."${serverDomain}".cert; + sslCertificateKey = certs."${serverDomain}".key; + }; + + security.pki.certificateFiles = [ certs.ca.cert ]; + + networking.hosts."::1" = [ "${serverDomain}" ]; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + }; + + nodes.client = + { pkgs, nodes, ... }: + { + networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ "${serverDomain}" ]; + + security.pki.certificateFiles = [ certs.ca.cert ]; + }; + + testScript = '' + start_all() + server.wait_for_unit("open-web-calendar.socket") + server.wait_until_succeeds("curl -f https://${serverDomain}/ | grep 'My custom title'") + ''; + } +)