diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index d55b5bfd42a7de..885dd9fb55865d 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -14,6 +14,8 @@ - [Kimai](https://www.kimai.org/), a web-based multi-user time-tracking application. Available as [services.kimai](option.html#opt-services.kimai). +- [Froide](https://github.com/okfde/froide), a freedom of information act web application. Available as [services.froide](#opt-services.froide.enable). + - [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](#opt-services.amazon-cloudwatch-agent.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5cadc0a5b0323f..5e97a5f25007e6 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1437,6 +1437,7 @@ ./services/web-apps/flarum.nix ./services/web-apps/fluidd.nix ./services/web-apps/freshrss.nix + ./services/web-apps/froide.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.nix b/nixos/modules/services/web-apps/froide.nix new file mode 100644 index 00000000000000..311343d950322f --- /dev/null +++ b/nixos/modules/services/web-apps/froide.nix @@ -0,0 +1,145 @@ +{ + config, + lib, + pkgs, + ... +}: +let + + cfg = config.services.froide; + format = pkgs.formats.toml { }; + +in +{ + options.services.froide = { + + enable = lib.mkEnableOption "Freedom of information portal web app Froide"; + + package = lib.mkPackageOption pkgs "froide" { }; + + port = lib.mkOption { + type = with lib.types; nullOr port; + default = 8001; + description = "Port to listen on."; + }; + + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/froide"; + description = "Directory to store the Froide server data."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to open ports in the firewall for the server. + ''; + }; + + settings = lib.mkOption { + default = { }; + description = '' + IMAP authentication configuration for rspamd-trainer. For supplying + the IMAP password, use the `secrets` option. + ''; + type = lib.types.submodule { freeformType = format.type; }; + example = lib.literalExpression '' + { + HOST = "localhost"; + USERNAME = "spam@example.com"; + INBOXPREFIX = "INBOX/"; + } + ''; + }; + + secrets = lib.mkOption { + type = with lib.types; listOf path; + description = '' + A list of files containing the various secrets. Should be in the + format expected by systemd's `EnvironmentFile` directory. For the + IMAP account password use `PASSWORD = mypassword`. + ''; + default = [ ]; + }; + + }; + + config = lib.mkIf cfg.enable { + + services.postgresql = { + enable = true; + ensureDatabases = [ "froide" ]; + ensureUsers = [ + { + name = "froide"; + ensureDBOwnership = true; + } + ]; + extraPlugins = ps: with ps; [ postgis ]; + authentication = '' + host froide froide localhost trust + ''; + initialScript = pkgs.writeText "backend-initScript" '' + ALTER USER froide WITH SUPERUSER; + ''; + }; + + systemd.services = { + + postgresql.serviceConfig.ExecStartPost = + let + sqlFile = pkgs.writeText "froide-pgvectors-setup.sql" '' + ALTER USER froide WITH SUPERUSER; + #CREATE EXTENSION IF NOT EXISTS postgis; + #ALTER SCHEMA govplan OWNER TO govplan; + #ALTER EXTENSION govplan UPDATE; + ''; + in + [ + '' + ${lib.getExe' config.services.postgresql.package "psql"} -d froide -f "${sqlFile}" + '' + ]; + + froide = { + description = "Gouvernment planer Govplan"; + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} runserver 0.0.0.0:8000"; + WorkingDirectory = cfg.dataDir; + StateDirectory = [ cfg.dataDir ]; + DynamicUser = true; + EnvironmentFile = [ + (format.generate "froide-env" cfg.settings) + cfg.secrets + ]; + Environment = [ "DJANGO_CONFIGURATION=Production" ]; + }; + after = [ "postgresql.service" ]; + wantedBy = [ "multi-user.target" ]; + preStart = '' + # Auto-migrate on first run or if the package has changed + versionFile="${cfg.dataDir}/src-version" + if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then + etebase-server migrate --no-input + etebase-server collectstatic --no-input --clear + echo ${cfg.package} > "$versionFile" + fi + # FIXME adapt paperless-ngx check for new versions + ${lib.getExe cfg.package} collectstatic + ${lib.getExe cfg.package} migrate + ''; + }; + }; + + environment.systemPackages = [ pkgs.froide ]; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + + }; + + meta.maintainers = with lib.maintainers; [ onny ]; + +} diff --git a/nixos/tests/froide.nix b/nixos/tests/froide.nix new file mode 100644 index 00000000000000..c8957806691406 --- /dev/null +++ b/nixos/tests/froide.nix @@ -0,0 +1,90 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "paperless"; + meta.maintainers = with lib.maintainers; [ leona SuperSandro2000 erikarvstedt ]; + + nodes = let self = { + simple = { pkgs, ... }: { + environment.systemPackages = with pkgs; [ imagemagick jq ]; + services.paperless = { + enable = true; + passwordFile = builtins.toFile "password" "admin"; + }; + }; + postgres = { config, pkgs, ... }: { + imports = [ self.simple ]; + services.postgresql = { + enable = true; + ensureDatabases = [ "paperless" ]; + ensureUsers = [ + { name = config.services.paperless.user; + ensureDBOwnership = true; + } + ]; + }; + services.paperless.settings = { + PAPERLESS_DBHOST = "/run/postgresql"; + PAPERLESS_OCR_LANGUAGE = "deu"; + }; + }; + }; in self; + + testScript = '' + import json + + def test_paperless(node): + node.wait_for_unit("paperless-consumer.service") + + with subtest("Add a document via the file system"): + node.succeed( + "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black " + "-annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png" + ) + + with subtest("Web interface gets ready"): + node.wait_for_unit("paperless-web.service") + # Wait until server accepts connections + node.wait_until_succeeds("curl -fs localhost:28981") + + # Required for consuming documents via the web interface + with subtest("Task-queue gets ready"): + node.wait_for_unit("paperless-task-queue.service") + + with subtest("Add a png document via the web interface"): + node.succeed( + "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black " + "-annotate +5+20 'hello web 16-10-2005' /tmp/webdoc.png" + ) + node.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.png -fs localhost:28981/api/documents/post_document/") + + with subtest("Add a txt document via the web interface"): + node.succeed( + "echo 'hello web 16-10-2005' > /tmp/webdoc.txt" + ) + node.wait_until_succeeds("curl -u admin:admin -F document=@/tmp/webdoc.txt -fs localhost:28981/api/documents/post_document/") + + with subtest("Documents are consumed"): + node.wait_until_succeeds( + "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 3))" + ) + docs = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/"))['results'] + assert "2005-10-16" in docs[0]['created'] + assert "2005-10-16" in docs[1]['created'] + assert "2005-10-16" in docs[2]['created'] + + # Detects gunicorn issues, see PR #190888 + with subtest("Document metadata can be accessed"): + metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/1/metadata/")) + assert "original_checksum" in metadata + + metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/2/metadata/")) + assert "original_checksum" in metadata + + metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/3/metadata/")) + assert "original_checksum" in metadata + + test_paperless(simple) + simple.send_monitor_command("quit") + simple.wait_for_shutdown() + test_paperless(postgres) + ''; +})