From 6200e654629cdfdacc2fa4c072809d3086198150 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Wed, 30 Oct 2024 08:14:23 +0100 Subject: [PATCH] nixos/froide-govplan: init --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/web-apps/froide-govplan.nix | 234 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/froide-govplan.nix | 36 +++ 5 files changed, 274 insertions(+) create mode 100644 nixos/modules/services/web-apps/froide-govplan.nix create mode 100644 nixos/tests/web-apps/froide-govplan.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 735d9945de6773..ca169d56902dc1 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -69,6 +69,8 @@ - [Whoogle Search](https://github.com/benbusby/whoogle-search), a self-hosted, ad-free, privacy-respecting metasearch engine. Available as [services.whoogle-search](options.html#opt-services.whoogle-search.enable). +- [Froide-Govplan](https://github.com/okfde/froide-govplan), a web application government planer. Available as [services.froide-govplan](#opt-services.froide-govplan.enable). + - [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable). - [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index be529602d6759f..119348a948f9d2 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1466,6 +1466,7 @@ ./services/web-apps/flarum.nix ./services/web-apps/fluidd.nix ./services/web-apps/freshrss.nix + ./services/web-apps/froide-govplan.nix ./services/web-apps/galene.nix ./services/web-apps/gancio.nix ./services/web-apps/gerrit.nix diff --git a/nixos/modules/services/web-apps/froide-govplan.nix b/nixos/modules/services/web-apps/froide-govplan.nix new file mode 100644 index 00000000000000..b6ac8abb037635 --- /dev/null +++ b/nixos/modules/services/web-apps/froide-govplan.nix @@ -0,0 +1,234 @@ +{ + config, + lib, + pkgs, + ... +}: +let + + cfg = config.services.froide-govplan; + pythonFmt = pkgs.formats.pythonVars { }; + settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings; + + pkg = cfg.package.overridePythonAttrs (old: { + postInstall = + old.postInstall + + '' + ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py + ''; + }); + + froide-govplan = pkgs.writeScriptBin "froide-govplan" '' + #! ${pkgs.runtimeShell} + sudo=exec + if [[ "$USER" != govplan ]]; then + sudo='exec /run/wrappers/bin/sudo -u govplan' + fi + $sudo ${pkgs.coreutils}/bin/env ${lib.getExe pkg} "$@" + ''; + + # Service hardening + defaultServiceConfig = { + # Secure the services + ReadWritePaths = [ cfg.dataDir ]; + CacheDirectory = "froide-govplan"; + CapabilityBoundingSet = ""; + # ProtectClock adds DeviceAllow=char-rtc r + DeviceAllow = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + ProtectSystem = "strict"; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged @setuid @keyring" + ]; + UMask = "0066"; + }; + +in +{ + options.services.froide-govplan = { + + enable = lib.mkEnableOption "Gouvernment planer web app Govplan"; + + package = lib.mkPackageOption pkgs "froide-govplan" { }; + + hostName = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "FQDN for the froide-govplan instance."; + }; + + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/froide-govplan"; + description = "Directory to store the Froide-Govplan server data."; + }; + + secretKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a file containing the secret key. + ''; + }; + + settings = lib.mkOption { + description = '' + Configuration options to set in `extra_settings.py`. + ''; + + default = { }; + + type = lib.types.submodule { + freeformType = pythonFmt.type; + + options = { + ALLOWED_HOSTS = lib.mkOption { + type = with lib.types; listOf str; + default = [ "*" ]; + description = '' + A list of valid fully-qualified domain names (FQDNs) and/or IP + addresses that can be used to reach the Froide-Govplan service. + ''; + }; + }; + }; + }; + + }; + + config = lib.mkIf cfg.enable { + + services.froide-govplan = { + settings = { + STATIC_ROOT = "${cfg.dataDir}/static"; + DEBUG = false; + DATABASES.default = { + ENGINE = "django.contrib.gis.db.backends.postgis"; + NAME = "govplan"; + USER = "govplan"; + HOST = "/run/postgresql"; + }; + }; + }; + + services.postgresql = { + enable = true; + ensureDatabases = [ "govplan" ]; + ensureUsers = [ + { + name = "govplan"; + ensureDBOwnership = true; + } + ]; + extensions = ps: with ps; [ postgis ]; + }; + + services.nginx = { + enable = lib.mkDefault true; + virtualHosts."${cfg.hostName}".locations = { + "/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;"; + "/static/".alias = "${cfg.dataDir}/static/"; + }; + proxyTimeout = lib.mkDefault "120s"; + }; + + systemd = { + services = { + + postgresql.serviceConfig.ExecStartPost = + let + sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" '' + CREATE EXTENSION IF NOT EXISTS postgis; + ''; + in + [ + '' + ${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}" + '' + ]; + + froide-govplan = { + description = "Gouvernment planer Govplan"; + serviceConfig = defaultServiceConfig // { + WorkingDirectory = cfg.dataDir; + StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan"; + User = "govplan"; + Group = "govplan"; + }; + after = [ + "postgresql.service" + "network.target" + "systemd-tmpfiles-setup.service" + ]; + wantedBy = [ "multi-user.target" ]; + environment = + { + PYTHONPATH = pkg.pythonPath; + GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so"; + GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so"; + } + // lib.optionalAttrs (cfg.secretKeyFile != null) { + SECRET_KEY_FILE = cfg.secretKeyFile; + }; + preStart = '' + # Auto-migrate on first run or if the package has changed + versionFile="${cfg.dataDir}/src-version" + version=$(cat "$versionFile" 2>/dev/null || echo 0) + + if [[ $version != ${pkg.version} ]]; then + ${lib.getExe pkg} migrate --no-input + ${lib.getExe pkg} collectstatic --no-input --clear + echo ${pkg.version} > "$versionFile" + fi + ''; + script = '' + ${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \ + --app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \ + project.asgi:application + ''; + }; + }; + + }; + + systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ]; + + environment.systemPackages = [ froide-govplan ]; + + users.users.govplan = { + home = "${cfg.dataDir}"; + isSystemUser = true; + group = "govplan"; + }; + users.groups.govplan = { }; + + }; + + meta.maintainers = with lib.maintainers; [ onny ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index be077353de00cf..72b24a9172631c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -366,6 +366,7 @@ in { freshrss-http-auth = handleTest ./freshrss-http-auth.nix {}; freshrss-none-auth = handleTest ./freshrss-none-auth.nix {}; frigate = handleTest ./frigate.nix {}; + froide-govplan = handleTest ./web-apps/froide-govplan.nix {}; frp = handleTest ./frp.nix {}; frr = handleTest ./frr.nix {}; fsck = handleTest ./fsck.nix {}; diff --git a/nixos/tests/web-apps/froide-govplan.nix b/nixos/tests/web-apps/froide-govplan.nix new file mode 100644 index 00000000000000..7b7ec3f7ccec36 --- /dev/null +++ b/nixos/tests/web-apps/froide-govplan.nix @@ -0,0 +1,36 @@ +import ../make-test-python.nix ( + { lib, pkgs, ... }: + { + name = "froide-govplan"; + meta.maintainers = with lib.maintainers; [ onny ]; + + nodes.machine = { config, ... }: { + virtualisation.memorySize = 2048; + services.froide-govplan.enable = true; + }; + + testScript = let + changePassword = pkgs.writeText "change-password.py" '' + from users.models import User + u = User.objects.get(username='govplan') + u.set_password('govplan') + u.save() + ''; + in '' + start_all() + machine.wait_for_unit("froide-govplan.service") + + with subtest("Home screen loads"): + machine.succeed( + "curl -sSfL http://[::1]:8080 | grep 'Home | NetBox'" + ) + + with subtest("Superuser can be created"): + machine.succeed( + "froide-govplan createsuperuser --noinput --username govplan --email govplan@example.com" + ) + # Django doesn't have a "clean" way of inputting the password from the command line + machine.succeed("cat '${changePassword}' | netbox-manage shell") + ''; + } +)