diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 957f80a7d12b..b20d1902c5c9 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -14,6 +14,8 @@ designed to run on affordable, low-power devices. Available as [services.meshtasticd] (#opt-services.meshtasticd.enable). +- [Goupile](https://goupile.org/en), an open-source design tool for secure forms including Clinical Report Forms (eCRF). Available as [services.goupile](#opt-services.goupile.enable). + - [knot-resolver](https://www.knot-resolver.cz/) in version 6. Available as `services.knot-resolver`. A module for knot-resolver 5 was already available as `services.kresd`. - [ImmichFrame](https://immichframe.dev/), display your photos from Immich as a digital photo frame. Available as `services.immichframe`. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9201540632cb..07de72b5173f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1651,6 +1651,7 @@ ./services/web-apps/goatcounter.nix ./services/web-apps/gotify-server.nix ./services/web-apps/gotosocial.nix + ./services/web-apps/goupile.nix ./services/web-apps/grav.nix ./services/web-apps/grocy.nix ./services/web-apps/guacamole-client.nix diff --git a/nixos/modules/services/web-apps/goupile.nix b/nixos/modules/services/web-apps/goupile.nix new file mode 100644 index 000000000000..0cb986e1c7f4 --- /dev/null +++ b/nixos/modules/services/web-apps/goupile.nix @@ -0,0 +1,147 @@ +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.services.goupile; + settingsFormat = pkgs.formats.ini { }; +in +{ + options.services.goupile = { + enable = lib.mkEnableOption "Goupile server"; + package = lib.mkPackageOption pkgs "goupile" { }; + + enableSandbox = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable the sandbox option."; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type; + options = { + HTTP.Port = lib.mkOption { + type = lib.types.port; + default = 8889; + description = "The port goupile runs on"; + }; + Data.RootDirectory = lib.mkOption { + type = lib.types.str; + default = "/var/lib/goupile"; + description = "Goupile's data directory."; + }; + }; + }; + default = { }; # default will be lost for submodules if overriden + example = lib.literalExpression '' + { + HTTP.Port = 8888; + } + ''; + description = '' + The options for `systemd.services.goupile` in ini format. + + The configuration options available can be found here + https://github.com/Koromix/rygel/blob/goupile/3.11.1/src/goupile/server/admin.cc#L41 + ''; + }; + + configFile = lib.mkOption { + type = lib.types.path; + description = '' + The configuration file to be passed to goupile server. + + By default the configuration file is created from `services.goupile.settings`. + ''; + }; + + hostName = lib.mkOption { + type = lib.types.str; + default = "goupile"; + description = "Nginx service name for goupile service."; + }; + }; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.nginx = { + enable = lib.mkDefault true; + virtualHosts.${cfg.hostName} = { + locations."/".proxyPass = "http://${cfg.hostName}:${builtins.toString cfg.settings.HTTP.Port}"; + }; + }; + } + { + services.goupile.configFile = settingsFormat.generate "goupile.ini" cfg.settings; + } + { + systemd.services.goupile = { + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + documentation = [ "https://goupile.org/en" ]; + description = "Goupile eCRF"; + + serviceConfig = { + ExecStart = '' + ${lib.getExe cfg.package} \ + ${lib.optionalString cfg.enableSandbox "--sandbox"} \ + -C ${cfg.configFile} + ''; + + DynamicUser = true; + + RuntimeDirectory = "goupile"; + RuntimeDirectoryPreserve = "yes"; + StateDirectory = "goupile"; + UMask = 0077; + WorkingDirectory = "%S/goupile"; + + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "~@privileged" + "~@resources" + "~@obsolete" + "~@mount" + "@system-service" + "@file-system" + "@basic-io" + "@clock" + ]; + + ProtectHome = true; + PrivateUsers = true; + PrivateDevices = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + + CapabilityBoundingSet = [ + "CAP_SYS_PTRACE" + "CAP_CHOWN" + "CAP_DAC_OVERRIDE" + "CAP_FOWNER" + "CAP_KILL" # Required for child process management + "CAP_NET_BIND_SERVICE" + "CAP_SETGID" + "CAP_SETUID" + "CAP_SYS_CHROOT" + "CAP_SYS_RESOURCE" + ]; + + Restart = "always"; + RestartSec = 20; + TimeoutStopSec = 30; + LimitNOFILE = 4096; + }; + }; + } + ] + ); + + meta.maintainers = lib.teams.ngi.members; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index cd7d3b61325c..0bf94d4c4ff2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -689,6 +689,7 @@ in gotenberg = runTest ./gotenberg.nix; gotify-server = runTest ./gotify-server.nix; gotosocial = runTest ./web-apps/gotosocial.nix; + goupile = runTest ./web-apps/goupile; grafana = handleTest ./grafana { }; graphite = runTest ./graphite.nix; grav = runTest ./web-apps/grav.nix; diff --git a/nixos/tests/web-apps/goupile/default.nix b/nixos/tests/web-apps/goupile/default.nix new file mode 100644 index 000000000000..f0d89e8c0777 --- /dev/null +++ b/nixos/tests/web-apps/goupile/default.nix @@ -0,0 +1,68 @@ +{ lib, pkgs, ... }: +{ + name = "goupile"; + + nodes.machine = + { + lib, + pkgs, + config, + ... + }: + { + services.goupile = { + enable = true; + enableSandbox = true; + settings.HTTP.Port = 8889; + }; + #systemd.services.goupile.environment.DEFAULT_SECCOMP_ACTION = "Log"; # Block|Log|Kill + networking = { + firewall.allowedTCPPorts = [ config.services.nginx.defaultHTTPListenPort ]; + hostName = "goupile"; + domain = "local"; + }; + }; + + testScript = + { nodes, ... }: + let + port = builtins.toString nodes.machine.services.goupile.settings.HTTP.Port; + in + # py + '' + start_all() + + machine.wait_for_unit("goupile.service") + machine.wait_for_open_port(${port}) + + machine.succeed("curl -q http://localhost:${port}") + machine.succeed("curl -q http://goupile.local") + machine.succeed("curl -q http://localhost") + ''; + + # Debug interactively with: + # - nix run .#nixosTests.goupile.driverInteractive -L + # - run_tests() + # ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.) + interactive.sshBackdoor.enable = true; + + interactive.nodes.machine = + { config, ... }: + let + port = config.services.goupile.settings.HTTP.Port; + in + { + virtualisation.forwardPorts = map (port: { + from = "host"; + host.port = port; + guest.port = port; + }) [ port ]; + + # forwarded ports need to be accessible + networking.firewall.allowedTCPPorts = [ port ]; + + virtualisation.graphics = false; + }; + + meta.maintainers = lib.teams.ngi.members; +}